##// END OF EJS Templates
hook: redirect stdout to stderr for ssh and http servers
Matt Mackall -
r5833:323b9c55 default
parent child Browse files
Show More
@@ -1,909 +1,910
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, mimetools, cStringIO
10 10 from mercurial.node import *
11 from mercurial import mdiff, ui, hg, util, archival, patch
11 from mercurial import mdiff, ui, hg, util, archival, patch, hook
12 12 from mercurial import revlog, templater
13 13 from common import ErrorResponse, get_mtime, style_map, paritygen, get_contact
14 14 from request import wsgirequest
15 15 import webcommands, protocol
16 16
17 17 shortcuts = {
18 18 'cl': [('cmd', ['changelog']), ('rev', None)],
19 19 'sl': [('cmd', ['shortlog']), ('rev', None)],
20 20 'cs': [('cmd', ['changeset']), ('node', None)],
21 21 'f': [('cmd', ['file']), ('filenode', None)],
22 22 'fl': [('cmd', ['filelog']), ('filenode', None)],
23 23 'fd': [('cmd', ['filediff']), ('node', None)],
24 24 'fa': [('cmd', ['annotate']), ('filenode', None)],
25 25 'mf': [('cmd', ['manifest']), ('manifest', None)],
26 26 'ca': [('cmd', ['archive']), ('node', None)],
27 27 'tags': [('cmd', ['tags'])],
28 28 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
29 29 'static': [('cmd', ['static']), ('file', None)]
30 30 }
31 31
32 32 def _up(p):
33 33 if p[0] != "/":
34 34 p = "/" + p
35 35 if p[-1] == "/":
36 36 p = p[:-1]
37 37 up = os.path.dirname(p)
38 38 if up == "/":
39 39 return "/"
40 40 return up + "/"
41 41
42 42 def revnavgen(pos, pagelen, limit, nodefunc):
43 43 def seq(factor, limit=None):
44 44 if limit:
45 45 yield limit
46 46 if limit >= 20 and limit <= 40:
47 47 yield 50
48 48 else:
49 49 yield 1 * factor
50 50 yield 3 * factor
51 51 for f in seq(factor * 10):
52 52 yield f
53 53
54 54 def nav(**map):
55 55 l = []
56 56 last = 0
57 57 for f in seq(1, pagelen):
58 58 if f < pagelen or f <= last:
59 59 continue
60 60 if f > limit:
61 61 break
62 62 last = f
63 63 if pos + f < limit:
64 64 l.append(("+%d" % f, hex(nodefunc(pos + f).node())))
65 65 if pos - f >= 0:
66 66 l.insert(0, ("-%d" % f, hex(nodefunc(pos - f).node())))
67 67
68 68 try:
69 69 yield {"label": "(0)", "node": hex(nodefunc('0').node())}
70 70
71 71 for label, node in l:
72 72 yield {"label": label, "node": node}
73 73
74 74 yield {"label": "tip", "node": "tip"}
75 75 except hg.RepoError:
76 76 pass
77 77
78 78 return nav
79 79
80 80 class hgweb(object):
81 81 def __init__(self, repo, name=None):
82 82 if isinstance(repo, str):
83 83 parentui = ui.ui(report_untrusted=False, interactive=False)
84 84 self.repo = hg.repository(parentui, repo)
85 85 else:
86 86 self.repo = repo
87 87
88 hook.redirect(True)
88 89 self.mtime = -1
89 90 self.reponame = name
90 91 self.archives = 'zip', 'gz', 'bz2'
91 92 self.stripecount = 1
92 93 # a repo owner may set web.templates in .hg/hgrc to get any file
93 94 # readable by the user running the CGI script
94 95 self.templatepath = self.config("web", "templates",
95 96 templater.templatepath(),
96 97 untrusted=False)
97 98
98 99 # The CGI scripts are often run by a user different from the repo owner.
99 100 # Trust the settings from the .hg/hgrc files by default.
100 101 def config(self, section, name, default=None, untrusted=True):
101 102 return self.repo.ui.config(section, name, default,
102 103 untrusted=untrusted)
103 104
104 105 def configbool(self, section, name, default=False, untrusted=True):
105 106 return self.repo.ui.configbool(section, name, default,
106 107 untrusted=untrusted)
107 108
108 109 def configlist(self, section, name, default=None, untrusted=True):
109 110 return self.repo.ui.configlist(section, name, default,
110 111 untrusted=untrusted)
111 112
112 113 def refresh(self):
113 114 mtime = get_mtime(self.repo.root)
114 115 if mtime != self.mtime:
115 116 self.mtime = mtime
116 117 self.repo = hg.repository(self.repo.ui, self.repo.root)
117 118 self.maxchanges = int(self.config("web", "maxchanges", 10))
118 119 self.stripecount = int(self.config("web", "stripes", 1))
119 120 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
120 121 self.maxfiles = int(self.config("web", "maxfiles", 10))
121 122 self.allowpull = self.configbool("web", "allowpull", True)
122 123 self.encoding = self.config("web", "encoding", util._encoding)
123 124
124 125 def run(self):
125 126 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
126 127 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
127 128 import mercurial.hgweb.wsgicgi as wsgicgi
128 129 wsgicgi.launch(self)
129 130
130 131 def __call__(self, env, respond):
131 132 req = wsgirequest(env, respond)
132 133 self.run_wsgi(req)
133 134 return req
134 135
135 136 def run_wsgi(self, req):
136 137
137 138 self.refresh()
138 139
139 140 # expand form shortcuts
140 141
141 142 for k in shortcuts.iterkeys():
142 143 if k in req.form:
143 144 for name, value in shortcuts[k]:
144 145 if value is None:
145 146 value = req.form[k]
146 147 req.form[name] = value
147 148 del req.form[k]
148 149
149 150 # work with CGI variables to create coherent structure
150 151 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
151 152
152 153 req.url = req.env['SCRIPT_NAME']
153 154 if not req.url.endswith('/'):
154 155 req.url += '/'
155 156 if req.env.has_key('REPO_NAME'):
156 157 req.url += req.env['REPO_NAME'] + '/'
157 158
158 159 if req.env.get('PATH_INFO'):
159 160 parts = req.env.get('PATH_INFO').strip('/').split('/')
160 161 repo_parts = req.env.get('REPO_NAME', '').split('/')
161 162 if parts[:len(repo_parts)] == repo_parts:
162 163 parts = parts[len(repo_parts):]
163 164 query = '/'.join(parts)
164 165 else:
165 166 query = req.env['QUERY_STRING'].split('&', 1)[0]
166 167 query = query.split(';', 1)[0]
167 168
168 169 # translate user-visible url structure to internal structure
169 170
170 171 args = query.split('/', 2)
171 172 if 'cmd' not in req.form and args and args[0]:
172 173
173 174 cmd = args.pop(0)
174 175 style = cmd.rfind('-')
175 176 if style != -1:
176 177 req.form['style'] = [cmd[:style]]
177 178 cmd = cmd[style+1:]
178 179
179 180 # avoid accepting e.g. style parameter as command
180 181 if hasattr(webcommands, cmd) or hasattr(protocol, cmd):
181 182 req.form['cmd'] = [cmd]
182 183
183 184 if args and args[0]:
184 185 node = args.pop(0)
185 186 req.form['node'] = [node]
186 187 if args:
187 188 req.form['file'] = args
188 189
189 190 if cmd == 'static':
190 191 req.form['file'] = req.form['node']
191 192 elif cmd == 'archive':
192 193 fn = req.form['node'][0]
193 194 for type_, spec in self.archive_specs.iteritems():
194 195 ext = spec[2]
195 196 if fn.endswith(ext):
196 197 req.form['node'] = [fn[:-len(ext)]]
197 198 req.form['type'] = [type_]
198 199
199 200 # actually process the request
200 201
201 202 try:
202 203
203 204 cmd = req.form.get('cmd', [''])[0]
204 205 if hasattr(protocol, cmd):
205 206 method = getattr(protocol, cmd)
206 207 method(self, req)
207 208 else:
208 209 tmpl = self.templater(req)
209 210 if cmd == '':
210 211 req.form['cmd'] = [tmpl.cache['default']]
211 212 cmd = req.form['cmd'][0]
212 213 method = getattr(webcommands, cmd)
213 214 method(self, req, tmpl)
214 215 del tmpl
215 216
216 217 except revlog.LookupError, err:
217 218 req.respond(404, tmpl(
218 219 'error', error='revision not found: %s' % err.name))
219 220 except (hg.RepoError, revlog.RevlogError), inst:
220 221 req.respond('500 Internal Server Error',
221 222 tmpl('error', error=str(inst)))
222 223 except ErrorResponse, inst:
223 224 req.respond(inst.code, tmpl('error', error=inst.message))
224 225 except AttributeError:
225 226 req.respond(400, tmpl('error', error='No such method: ' + cmd))
226 227
227 228 def templater(self, req):
228 229
229 230 # determine scheme, port and server name
230 231 # this is needed to create absolute urls
231 232
232 233 proto = req.env.get('wsgi.url_scheme')
233 234 if proto == 'https':
234 235 proto = 'https'
235 236 default_port = "443"
236 237 else:
237 238 proto = 'http'
238 239 default_port = "80"
239 240
240 241 port = req.env["SERVER_PORT"]
241 242 port = port != default_port and (":" + port) or ""
242 243 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
243 244 staticurl = self.config("web", "staticurl") or req.url + 'static/'
244 245 if not staticurl.endswith('/'):
245 246 staticurl += '/'
246 247
247 248 # some functions for the templater
248 249
249 250 def header(**map):
250 251 header_file = cStringIO.StringIO(
251 252 ''.join(tmpl("header", encoding=self.encoding, **map)))
252 253 msg = mimetools.Message(header_file, 0)
253 254 req.header(msg.items())
254 255 yield header_file.read()
255 256
256 257 def rawfileheader(**map):
257 258 req.header([('Content-type', map['mimetype']),
258 259 ('Content-disposition', 'filename=%s' % map['file']),
259 260 ('Content-length', str(len(map['raw'])))])
260 261 yield ''
261 262
262 263 def footer(**map):
263 264 yield tmpl("footer", **map)
264 265
265 266 def motd(**map):
266 267 yield self.config("web", "motd", "")
267 268
268 269 def sessionvars(**map):
269 270 fields = []
270 271 if req.form.has_key('style'):
271 272 style = req.form['style'][0]
272 273 if style != self.config('web', 'style', ''):
273 274 fields.append(('style', style))
274 275
275 276 separator = req.url[-1] == '?' and ';' or '?'
276 277 for name, value in fields:
277 278 yield dict(name=name, value=value, separator=separator)
278 279 separator = ';'
279 280
280 281 # figure out which style to use
281 282
282 283 style = self.config("web", "style", "")
283 284 if req.form.has_key('style'):
284 285 style = req.form['style'][0]
285 286 mapfile = style_map(self.templatepath, style)
286 287
287 288 if not self.reponame:
288 289 self.reponame = (self.config("web", "name")
289 290 or req.env.get('REPO_NAME')
290 291 or req.url.strip('/') or self.repo.root)
291 292
292 293 # create the templater
293 294
294 295 tmpl = templater.templater(mapfile, templater.common_filters,
295 296 defaults={"url": req.url,
296 297 "staticurl": staticurl,
297 298 "urlbase": urlbase,
298 299 "repo": self.reponame,
299 300 "header": header,
300 301 "footer": footer,
301 302 "motd": motd,
302 303 "rawfileheader": rawfileheader,
303 304 "sessionvars": sessionvars
304 305 })
305 306 return tmpl
306 307
307 308 def archivelist(self, nodeid):
308 309 allowed = self.configlist("web", "allow_archive")
309 310 for i, spec in self.archive_specs.iteritems():
310 311 if i in allowed or self.configbool("web", "allow" + i):
311 312 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
312 313
313 314 def listfilediffs(self, tmpl, files, changeset):
314 315 for f in files[:self.maxfiles]:
315 316 yield tmpl("filedifflink", node=hex(changeset), file=f)
316 317 if len(files) > self.maxfiles:
317 318 yield tmpl("fileellipses")
318 319
319 320 def siblings(self, siblings=[], hiderev=None, **args):
320 321 siblings = [s for s in siblings if s.node() != nullid]
321 322 if len(siblings) == 1 and siblings[0].rev() == hiderev:
322 323 return
323 324 for s in siblings:
324 325 d = {'node': hex(s.node()), 'rev': s.rev()}
325 326 if hasattr(s, 'path'):
326 327 d['file'] = s.path()
327 328 d.update(args)
328 329 yield d
329 330
330 331 def renamelink(self, fl, node):
331 332 r = fl.renamed(node)
332 333 if r:
333 334 return [dict(file=r[0], node=hex(r[1]))]
334 335 return []
335 336
336 337 def nodetagsdict(self, node):
337 338 return [{"name": i} for i in self.repo.nodetags(node)]
338 339
339 340 def nodebranchdict(self, ctx):
340 341 branches = []
341 342 branch = ctx.branch()
342 343 # If this is an empty repo, ctx.node() == nullid,
343 344 # ctx.branch() == 'default', but branchtags() is
344 345 # an empty dict. Using dict.get avoids a traceback.
345 346 if self.repo.branchtags().get(branch) == ctx.node():
346 347 branches.append({"name": branch})
347 348 return branches
348 349
349 350 def showtag(self, tmpl, t1, node=nullid, **args):
350 351 for t in self.repo.nodetags(node):
351 352 yield tmpl(t1, tag=t, **args)
352 353
353 354 def diff(self, tmpl, node1, node2, files):
354 355 def filterfiles(filters, files):
355 356 l = [x for x in files if x in filters]
356 357
357 358 for t in filters:
358 359 if t and t[-1] != os.sep:
359 360 t += os.sep
360 361 l += [x for x in files if x.startswith(t)]
361 362 return l
362 363
363 364 parity = paritygen(self.stripecount)
364 365 def diffblock(diff, f, fn):
365 366 yield tmpl("diffblock",
366 367 lines=prettyprintlines(diff),
367 368 parity=parity.next(),
368 369 file=f,
369 370 filenode=hex(fn or nullid))
370 371
371 372 def prettyprintlines(diff):
372 373 for l in diff.splitlines(1):
373 374 if l.startswith('+'):
374 375 yield tmpl("difflineplus", line=l)
375 376 elif l.startswith('-'):
376 377 yield tmpl("difflineminus", line=l)
377 378 elif l.startswith('@'):
378 379 yield tmpl("difflineat", line=l)
379 380 else:
380 381 yield tmpl("diffline", line=l)
381 382
382 383 r = self.repo
383 384 c1 = r.changectx(node1)
384 385 c2 = r.changectx(node2)
385 386 date1 = util.datestr(c1.date())
386 387 date2 = util.datestr(c2.date())
387 388
388 389 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
389 390 if files:
390 391 modified, added, removed = map(lambda x: filterfiles(files, x),
391 392 (modified, added, removed))
392 393
393 394 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
394 395 for f in modified:
395 396 to = c1.filectx(f).data()
396 397 tn = c2.filectx(f).data()
397 398 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
398 399 opts=diffopts), f, tn)
399 400 for f in added:
400 401 to = None
401 402 tn = c2.filectx(f).data()
402 403 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
403 404 opts=diffopts), f, tn)
404 405 for f in removed:
405 406 to = c1.filectx(f).data()
406 407 tn = None
407 408 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
408 409 opts=diffopts), f, tn)
409 410
410 411 def changelog(self, tmpl, ctx, shortlog=False):
411 412 def changelist(limit=0,**map):
412 413 cl = self.repo.changelog
413 414 l = [] # build a list in forward order for efficiency
414 415 for i in xrange(start, end):
415 416 ctx = self.repo.changectx(i)
416 417 n = ctx.node()
417 418
418 419 l.insert(0, {"parity": parity.next(),
419 420 "author": ctx.user(),
420 421 "parent": self.siblings(ctx.parents(), i - 1),
421 422 "child": self.siblings(ctx.children(), i + 1),
422 423 "changelogtag": self.showtag("changelogtag",n),
423 424 "desc": ctx.description(),
424 425 "date": ctx.date(),
425 426 "files": self.listfilediffs(tmpl, ctx.files(), n),
426 427 "rev": i,
427 428 "node": hex(n),
428 429 "tags": self.nodetagsdict(n),
429 430 "branches": self.nodebranchdict(ctx)})
430 431
431 432 if limit > 0:
432 433 l = l[:limit]
433 434
434 435 for e in l:
435 436 yield e
436 437
437 438 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
438 439 cl = self.repo.changelog
439 440 count = cl.count()
440 441 pos = ctx.rev()
441 442 start = max(0, pos - maxchanges + 1)
442 443 end = min(count, start + maxchanges)
443 444 pos = end - 1
444 445 parity = paritygen(self.stripecount, offset=start-end)
445 446
446 447 changenav = revnavgen(pos, maxchanges, count, self.repo.changectx)
447 448
448 449 yield tmpl(shortlog and 'shortlog' or 'changelog',
449 450 changenav=changenav,
450 451 node=hex(cl.tip()),
451 452 rev=pos, changesets=count,
452 453 entries=lambda **x: changelist(limit=0,**x),
453 454 latestentry=lambda **x: changelist(limit=1,**x),
454 455 archives=self.archivelist("tip"))
455 456
456 457 def search(self, tmpl, query):
457 458
458 459 def changelist(**map):
459 460 cl = self.repo.changelog
460 461 count = 0
461 462 qw = query.lower().split()
462 463
463 464 def revgen():
464 465 for i in xrange(cl.count() - 1, 0, -100):
465 466 l = []
466 467 for j in xrange(max(0, i - 100), i):
467 468 ctx = self.repo.changectx(j)
468 469 l.append(ctx)
469 470 l.reverse()
470 471 for e in l:
471 472 yield e
472 473
473 474 for ctx in revgen():
474 475 miss = 0
475 476 for q in qw:
476 477 if not (q in ctx.user().lower() or
477 478 q in ctx.description().lower() or
478 479 q in " ".join(ctx.files()).lower()):
479 480 miss = 1
480 481 break
481 482 if miss:
482 483 continue
483 484
484 485 count += 1
485 486 n = ctx.node()
486 487
487 488 yield tmpl('searchentry',
488 489 parity=parity.next(),
489 490 author=ctx.user(),
490 491 parent=self.siblings(ctx.parents()),
491 492 child=self.siblings(ctx.children()),
492 493 changelogtag=self.showtag("changelogtag",n),
493 494 desc=ctx.description(),
494 495 date=ctx.date(),
495 496 files=self.listfilediffs(tmpl, ctx.files(), n),
496 497 rev=ctx.rev(),
497 498 node=hex(n),
498 499 tags=self.nodetagsdict(n),
499 500 branches=self.nodebranchdict(ctx))
500 501
501 502 if count >= self.maxchanges:
502 503 break
503 504
504 505 cl = self.repo.changelog
505 506 parity = paritygen(self.stripecount)
506 507
507 508 yield tmpl('search',
508 509 query=query,
509 510 node=hex(cl.tip()),
510 511 entries=changelist,
511 512 archives=self.archivelist("tip"))
512 513
513 514 def changeset(self, tmpl, ctx):
514 515 n = ctx.node()
515 516 parents = ctx.parents()
516 517 p1 = parents[0].node()
517 518
518 519 files = []
519 520 parity = paritygen(self.stripecount)
520 521 for f in ctx.files():
521 522 files.append(tmpl("filenodelink",
522 523 node=hex(n), file=f,
523 524 parity=parity.next()))
524 525
525 526 def diff(**map):
526 527 yield self.diff(tmpl, p1, n, None)
527 528
528 529 yield tmpl('changeset',
529 530 diff=diff,
530 531 rev=ctx.rev(),
531 532 node=hex(n),
532 533 parent=self.siblings(parents),
533 534 child=self.siblings(ctx.children()),
534 535 changesettag=self.showtag("changesettag",n),
535 536 author=ctx.user(),
536 537 desc=ctx.description(),
537 538 date=ctx.date(),
538 539 files=files,
539 540 archives=self.archivelist(hex(n)),
540 541 tags=self.nodetagsdict(n),
541 542 branches=self.nodebranchdict(ctx))
542 543
543 544 def filelog(self, tmpl, fctx):
544 545 f = fctx.path()
545 546 fl = fctx.filelog()
546 547 count = fl.count()
547 548 pagelen = self.maxshortchanges
548 549 pos = fctx.filerev()
549 550 start = max(0, pos - pagelen + 1)
550 551 end = min(count, start + pagelen)
551 552 pos = end - 1
552 553 parity = paritygen(self.stripecount, offset=start-end)
553 554
554 555 def entries(limit=0, **map):
555 556 l = []
556 557
557 558 for i in xrange(start, end):
558 559 ctx = fctx.filectx(i)
559 560 n = fl.node(i)
560 561
561 562 l.insert(0, {"parity": parity.next(),
562 563 "filerev": i,
563 564 "file": f,
564 565 "node": hex(ctx.node()),
565 566 "author": ctx.user(),
566 567 "date": ctx.date(),
567 568 "rename": self.renamelink(fl, n),
568 569 "parent": self.siblings(fctx.parents()),
569 570 "child": self.siblings(fctx.children()),
570 571 "desc": ctx.description()})
571 572
572 573 if limit > 0:
573 574 l = l[:limit]
574 575
575 576 for e in l:
576 577 yield e
577 578
578 579 nodefunc = lambda x: fctx.filectx(fileid=x)
579 580 nav = revnavgen(pos, pagelen, count, nodefunc)
580 581 yield tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav,
581 582 entries=lambda **x: entries(limit=0, **x),
582 583 latestentry=lambda **x: entries(limit=1, **x))
583 584
584 585 def filerevision(self, tmpl, fctx):
585 586 f = fctx.path()
586 587 text = fctx.data()
587 588 fl = fctx.filelog()
588 589 n = fctx.filenode()
589 590 parity = paritygen(self.stripecount)
590 591
591 592 mt = mimetypes.guess_type(f)[0]
592 593 rawtext = text
593 594 if util.binary(text):
594 595 mt = mt or 'application/octet-stream'
595 596 text = "(binary:%s)" % mt
596 597 mt = mt or 'text/plain'
597 598
598 599 def lines():
599 600 for l, t in enumerate(text.splitlines(1)):
600 601 yield {"line": t,
601 602 "linenumber": "% 6d" % (l + 1),
602 603 "parity": parity.next()}
603 604
604 605 yield tmpl("filerevision",
605 606 file=f,
606 607 path=_up(f),
607 608 text=lines(),
608 609 raw=rawtext,
609 610 mimetype=mt,
610 611 rev=fctx.rev(),
611 612 node=hex(fctx.node()),
612 613 author=fctx.user(),
613 614 date=fctx.date(),
614 615 desc=fctx.description(),
615 616 parent=self.siblings(fctx.parents()),
616 617 child=self.siblings(fctx.children()),
617 618 rename=self.renamelink(fl, n),
618 619 permissions=fctx.manifest().flags(f))
619 620
620 621 def fileannotate(self, tmpl, fctx):
621 622 f = fctx.path()
622 623 n = fctx.filenode()
623 624 fl = fctx.filelog()
624 625 parity = paritygen(self.stripecount)
625 626
626 627 def annotate(**map):
627 628 last = None
628 629 for f, l in fctx.annotate(follow=True):
629 630 fnode = f.filenode()
630 631 name = self.repo.ui.shortuser(f.user())
631 632
632 633 if last != fnode:
633 634 last = fnode
634 635
635 636 yield {"parity": parity.next(),
636 637 "node": hex(f.node()),
637 638 "rev": f.rev(),
638 639 "author": name,
639 640 "file": f.path(),
640 641 "line": l}
641 642
642 643 yield tmpl("fileannotate",
643 644 file=f,
644 645 annotate=annotate,
645 646 path=_up(f),
646 647 rev=fctx.rev(),
647 648 node=hex(fctx.node()),
648 649 author=fctx.user(),
649 650 date=fctx.date(),
650 651 desc=fctx.description(),
651 652 rename=self.renamelink(fl, n),
652 653 parent=self.siblings(fctx.parents()),
653 654 child=self.siblings(fctx.children()),
654 655 permissions=fctx.manifest().flags(f))
655 656
656 657 def manifest(self, tmpl, ctx, path):
657 658 mf = ctx.manifest()
658 659 node = ctx.node()
659 660
660 661 files = {}
661 662 parity = paritygen(self.stripecount)
662 663
663 664 if path and path[-1] != "/":
664 665 path += "/"
665 666 l = len(path)
666 667 abspath = "/" + path
667 668
668 669 for f, n in mf.items():
669 670 if f[:l] != path:
670 671 continue
671 672 remain = f[l:]
672 673 if "/" in remain:
673 674 short = remain[:remain.index("/") + 1] # bleah
674 675 files[short] = (f, None)
675 676 else:
676 677 short = os.path.basename(remain)
677 678 files[short] = (f, n)
678 679
679 680 if not files:
680 681 raise ErrorResponse(404, 'Path not found: ' + path)
681 682
682 683 def filelist(**map):
683 684 fl = files.keys()
684 685 fl.sort()
685 686 for f in fl:
686 687 full, fnode = files[f]
687 688 if not fnode:
688 689 continue
689 690
690 691 fctx = ctx.filectx(full)
691 692 yield {"file": full,
692 693 "parity": parity.next(),
693 694 "basename": f,
694 695 "date": fctx.changectx().date(),
695 696 "size": fctx.size(),
696 697 "permissions": mf.flags(full)}
697 698
698 699 def dirlist(**map):
699 700 fl = files.keys()
700 701 fl.sort()
701 702 for f in fl:
702 703 full, fnode = files[f]
703 704 if fnode:
704 705 continue
705 706
706 707 yield {"parity": parity.next(),
707 708 "path": "%s%s" % (abspath, f),
708 709 "basename": f[:-1]}
709 710
710 711 yield tmpl("manifest",
711 712 rev=ctx.rev(),
712 713 node=hex(node),
713 714 path=abspath,
714 715 up=_up(abspath),
715 716 upparity=parity.next(),
716 717 fentries=filelist,
717 718 dentries=dirlist,
718 719 archives=self.archivelist(hex(node)),
719 720 tags=self.nodetagsdict(node),
720 721 branches=self.nodebranchdict(ctx))
721 722
722 723 def tags(self, tmpl):
723 724 i = self.repo.tagslist()
724 725 i.reverse()
725 726 parity = paritygen(self.stripecount)
726 727
727 728 def entries(notip=False,limit=0, **map):
728 729 count = 0
729 730 for k, n in i:
730 731 if notip and k == "tip":
731 732 continue
732 733 if limit > 0 and count >= limit:
733 734 continue
734 735 count = count + 1
735 736 yield {"parity": parity.next(),
736 737 "tag": k,
737 738 "date": self.repo.changectx(n).date(),
738 739 "node": hex(n)}
739 740
740 741 yield tmpl("tags",
741 742 node=hex(self.repo.changelog.tip()),
742 743 entries=lambda **x: entries(False,0, **x),
743 744 entriesnotip=lambda **x: entries(True,0, **x),
744 745 latestentry=lambda **x: entries(True,1, **x))
745 746
746 747 def summary(self, tmpl):
747 748 i = self.repo.tagslist()
748 749 i.reverse()
749 750
750 751 def tagentries(**map):
751 752 parity = paritygen(self.stripecount)
752 753 count = 0
753 754 for k, n in i:
754 755 if k == "tip": # skip tip
755 756 continue;
756 757
757 758 count += 1
758 759 if count > 10: # limit to 10 tags
759 760 break;
760 761
761 762 yield tmpl("tagentry",
762 763 parity=parity.next(),
763 764 tag=k,
764 765 node=hex(n),
765 766 date=self.repo.changectx(n).date())
766 767
767 768
768 769 def branches(**map):
769 770 parity = paritygen(self.stripecount)
770 771
771 772 b = self.repo.branchtags()
772 773 l = [(-self.repo.changelog.rev(n), n, t) for t, n in b.items()]
773 774 l.sort()
774 775
775 776 for r,n,t in l:
776 777 ctx = self.repo.changectx(n)
777 778
778 779 yield {'parity': parity.next(),
779 780 'branch': t,
780 781 'node': hex(n),
781 782 'date': ctx.date()}
782 783
783 784 def changelist(**map):
784 785 parity = paritygen(self.stripecount, offset=start-end)
785 786 l = [] # build a list in forward order for efficiency
786 787 for i in xrange(start, end):
787 788 ctx = self.repo.changectx(i)
788 789 n = ctx.node()
789 790 hn = hex(n)
790 791
791 792 l.insert(0, tmpl(
792 793 'shortlogentry',
793 794 parity=parity.next(),
794 795 author=ctx.user(),
795 796 desc=ctx.description(),
796 797 date=ctx.date(),
797 798 rev=i,
798 799 node=hn,
799 800 tags=self.nodetagsdict(n),
800 801 branches=self.nodebranchdict(ctx)))
801 802
802 803 yield l
803 804
804 805 cl = self.repo.changelog
805 806 count = cl.count()
806 807 start = max(0, count - self.maxchanges)
807 808 end = min(count, start + self.maxchanges)
808 809
809 810 yield tmpl("summary",
810 811 desc=self.config("web", "description", "unknown"),
811 812 owner=get_contact(self.config) or "unknown",
812 813 lastchange=cl.read(cl.tip())[2],
813 814 tags=tagentries,
814 815 branches=branches,
815 816 shortlog=changelist,
816 817 node=hex(cl.tip()),
817 818 archives=self.archivelist("tip"))
818 819
819 820 def filediff(self, tmpl, fctx):
820 821 n = fctx.node()
821 822 path = fctx.path()
822 823 parents = fctx.parents()
823 824 p1 = parents and parents[0].node() or nullid
824 825
825 826 def diff(**map):
826 827 yield self.diff(tmpl, p1, n, [path])
827 828
828 829 yield tmpl("filediff",
829 830 file=path,
830 831 node=hex(n),
831 832 rev=fctx.rev(),
832 833 parent=self.siblings(parents),
833 834 child=self.siblings(fctx.children()),
834 835 diff=diff)
835 836
836 837 archive_specs = {
837 838 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
838 839 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
839 840 'zip': ('application/zip', 'zip', '.zip', None),
840 841 }
841 842
842 843 def archive(self, tmpl, req, key, type_):
843 844 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
844 845 cnode = self.repo.lookup(key)
845 846 arch_version = key
846 847 if cnode == key or key == 'tip':
847 848 arch_version = short(cnode)
848 849 name = "%s-%s" % (reponame, arch_version)
849 850 mimetype, artype, extension, encoding = self.archive_specs[type_]
850 851 headers = [('Content-type', mimetype),
851 852 ('Content-disposition', 'attachment; filename=%s%s' %
852 853 (name, extension))]
853 854 if encoding:
854 855 headers.append(('Content-encoding', encoding))
855 856 req.header(headers)
856 857 archival.archive(self.repo, req.out, cnode, artype, prefix=name)
857 858
858 859 # add tags to things
859 860 # tags -> list of changesets corresponding to tags
860 861 # find tag, changeset, file
861 862
862 863 def cleanpath(self, path):
863 864 path = path.lstrip('/')
864 865 return util.canonpath(self.repo.root, '', path)
865 866
866 867 def changectx(self, req):
867 868 if req.form.has_key('node'):
868 869 changeid = req.form['node'][0]
869 870 elif req.form.has_key('manifest'):
870 871 changeid = req.form['manifest'][0]
871 872 else:
872 873 changeid = self.repo.changelog.count() - 1
873 874
874 875 try:
875 876 ctx = self.repo.changectx(changeid)
876 877 except hg.RepoError:
877 878 man = self.repo.manifest
878 879 mn = man.lookup(changeid)
879 880 ctx = self.repo.changectx(man.linkrev(mn))
880 881
881 882 return ctx
882 883
883 884 def filectx(self, req):
884 885 path = self.cleanpath(req.form['file'][0])
885 886 if req.form.has_key('node'):
886 887 changeid = req.form['node'][0]
887 888 else:
888 889 changeid = req.form['filenode'][0]
889 890 try:
890 891 ctx = self.repo.changectx(changeid)
891 892 fctx = ctx.filectx(path)
892 893 except hg.RepoError:
893 894 fctx = self.repo.filectx(path, fileid=changeid)
894 895
895 896 return fctx
896 897
897 898 def check_perm(self, req, op, default):
898 899 '''check permission for operation based on user auth.
899 900 return true if op allowed, else false.
900 901 default is policy to use if no config given.'''
901 902
902 903 user = req.env.get('REMOTE_USER')
903 904
904 905 deny = self.configlist('web', 'deny_' + op)
905 906 if deny and (not user or deny == ['*'] or user in deny):
906 907 return False
907 908
908 909 allow = self.configlist('web', 'allow_' + op)
909 910 return (allow and (allow == ['*'] or user in allow)) or default
@@ -1,96 +1,109
1 1 # hook.py - hook support for mercurial
2 2 #
3 3 # Copyright 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 from i18n import _
9 import util
9 import util, os, sys
10 10
11 11 def _pythonhook(ui, repo, name, hname, funcname, args, throw):
12 12 '''call python hook. hook is callable object, looked up as
13 13 name in python module. if callable returns "true", hook
14 14 fails, else passes. if hook raises exception, treated as
15 15 hook failure. exception propagates if throw is "true".
16 16
17 17 reason for "true" meaning "hook failed" is so that
18 18 unmodified commands (e.g. mercurial.commands.update) can
19 19 be run as hooks without wrappers to convert return values.'''
20 20
21 21 ui.note(_("calling hook %s: %s\n") % (hname, funcname))
22 22 obj = funcname
23 23 if not callable(obj):
24 24 d = funcname.rfind('.')
25 25 if d == -1:
26 26 raise util.Abort(_('%s hook is invalid ("%s" not in '
27 27 'a module)') % (hname, funcname))
28 28 modname = funcname[:d]
29 29 try:
30 30 obj = __import__(modname)
31 31 except ImportError:
32 32 try:
33 33 # extensions are loaded with hgext_ prefix
34 34 obj = __import__("hgext_%s" % modname)
35 35 except ImportError:
36 36 raise util.Abort(_('%s hook is invalid '
37 37 '(import of "%s" failed)') %
38 38 (hname, modname))
39 39 try:
40 40 for p in funcname.split('.')[1:]:
41 41 obj = getattr(obj, p)
42 42 except AttributeError, err:
43 43 raise util.Abort(_('%s hook is invalid '
44 44 '("%s" is not defined)') %
45 45 (hname, funcname))
46 46 if not callable(obj):
47 47 raise util.Abort(_('%s hook is invalid '
48 48 '("%s" is not callable)') %
49 49 (hname, funcname))
50 50 try:
51 51 r = obj(ui=ui, repo=repo, hooktype=name, **args)
52 52 except (KeyboardInterrupt, util.SignalInterrupt):
53 53 raise
54 54 except Exception, exc:
55 55 if isinstance(exc, util.Abort):
56 56 ui.warn(_('error: %s hook failed: %s\n') %
57 57 (hname, exc.args[0]))
58 58 else:
59 59 ui.warn(_('error: %s hook raised an exception: '
60 60 '%s\n') % (hname, exc))
61 61 if throw:
62 62 raise
63 63 ui.print_exc()
64 64 return True
65 65 if r:
66 66 if throw:
67 67 raise util.Abort(_('%s hook failed') % hname)
68 68 ui.warn(_('warning: %s hook failed\n') % hname)
69 69 return r
70 70
71 71 def _exthook(ui, repo, name, cmd, args, throw):
72 72 ui.note(_("running hook %s: %s\n") % (name, cmd))
73 73 env = dict([('HG_' + k.upper(), v) for k, v in args.iteritems()])
74 74 r = util.system(cmd, environ=env, cwd=repo.root)
75 75 if r:
76 76 desc, r = util.explain_exit(r)
77 77 if throw:
78 78 raise util.Abort(_('%s hook %s') % (name, desc))
79 79 ui.warn(_('warning: %s hook %s\n') % (name, desc))
80 80 return r
81 81
82 _redirect = False
83 def redirect(state):
84 _redirect = state
85
82 86 def hook(ui, repo, name, throw=False, **args):
83 87 r = False
88
89 if _redirect:
90 # temporarily redirect stdout to stderr
91 oldstdout = os.dup(sys.stdout.fileno())
92 os.dup2(sys.stderr.fileno(), sys.stdout.fileno())
93
84 94 hooks = [(hname, cmd) for hname, cmd in ui.configitems("hooks")
85 95 if hname.split(".", 1)[0] == name and cmd]
86 96 hooks.sort()
87 97 for hname, cmd in hooks:
88 98 if callable(cmd):
89 99 r = _pythonhook(ui, repo, name, hname, cmd, args, throw) or r
90 100 elif cmd.startswith('python:'):
91 101 r = _pythonhook(ui, repo, name, hname, cmd[7:].strip(),
92 102 args, throw) or r
93 103 else:
94 104 r = _exthook(ui, repo, hname, cmd, args, throw) or r
95 105 return r
96 106
107 if _redirect:
108 os.dup2(oldstdout, sys.stdout.fileno())
109 os.close(oldstdout)
@@ -1,204 +1,205
1 1 # sshserver.py - ssh protocol server support for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.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 from i18n import _
10 10 from node import *
11 import os, streamclone, sys, tempfile, util
11 import os, streamclone, sys, tempfile, util, hook
12 12
13 13 class sshserver(object):
14 14 def __init__(self, ui, repo):
15 15 self.ui = ui
16 16 self.repo = repo
17 17 self.lock = None
18 18 self.fin = sys.stdin
19 19 self.fout = sys.stdout
20 20
21 hook.redirect(True)
21 22 sys.stdout = sys.stderr
22 23
23 24 # Prevent insertion/deletion of CRs
24 25 util.set_binary(self.fin)
25 26 util.set_binary(self.fout)
26 27
27 28 def getarg(self):
28 29 argline = self.fin.readline()[:-1]
29 30 arg, l = argline.split()
30 31 val = self.fin.read(int(l))
31 32 return arg, val
32 33
33 34 def respond(self, v):
34 35 self.fout.write("%d\n" % len(v))
35 36 self.fout.write(v)
36 37 self.fout.flush()
37 38
38 39 def serve_forever(self):
39 40 while self.serve_one(): pass
40 41 sys.exit(0)
41 42
42 43 def serve_one(self):
43 44 cmd = self.fin.readline()[:-1]
44 45 if cmd:
45 46 impl = getattr(self, 'do_' + cmd, None)
46 47 if impl: impl()
47 48 else: self.respond("")
48 49 return cmd != ''
49 50
50 51 def do_lookup(self):
51 52 arg, key = self.getarg()
52 53 assert arg == 'key'
53 54 try:
54 55 r = hex(self.repo.lookup(key))
55 56 success = 1
56 57 except Exception,inst:
57 58 r = str(inst)
58 59 success = 0
59 60 self.respond("%s %s\n" % (success, r))
60 61
61 62 def do_heads(self):
62 63 h = self.repo.heads()
63 64 self.respond(" ".join(map(hex, h)) + "\n")
64 65
65 66 def do_hello(self):
66 67 '''the hello command returns a set of lines describing various
67 68 interesting things about the server, in an RFC822-like format.
68 69 Currently the only one defined is "capabilities", which
69 70 consists of a line in the form:
70 71
71 72 capabilities: space separated list of tokens
72 73 '''
73 74
74 75 caps = ['unbundle', 'lookup', 'changegroupsubset']
75 76 if self.ui.configbool('server', 'uncompressed'):
76 77 caps.append('stream=%d' % self.repo.changelog.version)
77 78 self.respond("capabilities: %s\n" % (' '.join(caps),))
78 79
79 80 def do_lock(self):
80 81 '''DEPRECATED - allowing remote client to lock repo is not safe'''
81 82
82 83 self.lock = self.repo.lock()
83 84 self.respond("")
84 85
85 86 def do_unlock(self):
86 87 '''DEPRECATED'''
87 88
88 89 if self.lock:
89 90 self.lock.release()
90 91 self.lock = None
91 92 self.respond("")
92 93
93 94 def do_branches(self):
94 95 arg, nodes = self.getarg()
95 96 nodes = map(bin, nodes.split(" "))
96 97 r = []
97 98 for b in self.repo.branches(nodes):
98 99 r.append(" ".join(map(hex, b)) + "\n")
99 100 self.respond("".join(r))
100 101
101 102 def do_between(self):
102 103 arg, pairs = self.getarg()
103 104 pairs = [map(bin, p.split("-")) for p in pairs.split(" ")]
104 105 r = []
105 106 for b in self.repo.between(pairs):
106 107 r.append(" ".join(map(hex, b)) + "\n")
107 108 self.respond("".join(r))
108 109
109 110 def do_changegroup(self):
110 111 nodes = []
111 112 arg, roots = self.getarg()
112 113 nodes = map(bin, roots.split(" "))
113 114
114 115 cg = self.repo.changegroup(nodes, 'serve')
115 116 while True:
116 117 d = cg.read(4096)
117 118 if not d:
118 119 break
119 120 self.fout.write(d)
120 121
121 122 self.fout.flush()
122 123
123 124 def do_changegroupsubset(self):
124 125 bases = []
125 126 heads = []
126 127 argmap = dict([self.getarg(), self.getarg()])
127 128 bases = [bin(n) for n in argmap['bases'].split(' ')]
128 129 heads = [bin(n) for n in argmap['heads'].split(' ')]
129 130
130 131 cg = self.repo.changegroupsubset(bases, heads, 'serve')
131 132 while True:
132 133 d = cg.read(4096)
133 134 if not d:
134 135 break
135 136 self.fout.write(d)
136 137
137 138 self.fout.flush()
138 139
139 140 def do_addchangegroup(self):
140 141 '''DEPRECATED'''
141 142
142 143 if not self.lock:
143 144 self.respond("not locked")
144 145 return
145 146
146 147 self.respond("")
147 148 r = self.repo.addchangegroup(self.fin, 'serve', self.client_url())
148 149 self.respond(str(r))
149 150
150 151 def client_url(self):
151 152 client = os.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
152 153 return 'remote:ssh:' + client
153 154
154 155 def do_unbundle(self):
155 156 their_heads = self.getarg()[1].split()
156 157
157 158 def check_heads():
158 159 heads = map(hex, self.repo.heads())
159 160 return their_heads == [hex('force')] or their_heads == heads
160 161
161 162 # fail early if possible
162 163 if not check_heads():
163 164 self.respond(_('unsynced changes'))
164 165 return
165 166
166 167 self.respond('')
167 168
168 169 # write bundle data to temporary file because it can be big
169 170
170 171 try:
171 172 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
172 173 fp = os.fdopen(fd, 'wb+')
173 174
174 175 count = int(self.fin.readline())
175 176 while count:
176 177 fp.write(self.fin.read(count))
177 178 count = int(self.fin.readline())
178 179
179 180 was_locked = self.lock is not None
180 181 if not was_locked:
181 182 self.lock = self.repo.lock()
182 183 try:
183 184 if not check_heads():
184 185 # someone else committed/pushed/unbundled while we
185 186 # were transferring data
186 187 self.respond(_('unsynced changes'))
187 188 return
188 189 self.respond('')
189 190
190 191 # push can proceed
191 192
192 193 fp.seek(0)
193 194 r = self.repo.addchangegroup(fp, 'serve', self.client_url())
194 195 self.respond(str(r))
195 196 finally:
196 197 if not was_locked:
197 198 self.lock.release()
198 199 self.lock = None
199 200 finally:
200 201 fp.close()
201 202 os.unlink(tempname)
202 203
203 204 def do_stream_out(self):
204 205 streamclone.stream_out(self.repo, self.fout)
General Comments 0
You need to be logged in to leave comments. Login now