##// END OF EJS Templates
hgwebdir: export collections of repos...
Vadim Gelfer -
r1829:b0f6af32 default
parent child Browse files
Show More
@@ -1,20 +1,31
1 1 #!/usr/bin/env python
2 2 #
3 3 # An example CGI script to export multiple hgweb repos, edit as necessary
4 4
5 5 import cgitb, sys
6 6 cgitb.enable()
7 7
8 8 # sys.path.insert(0, "/path/to/python/lib") # if not a system-wide install
9 9 from mercurial import hgweb
10 10
11 # The config file looks like this:
11 # The config file looks like this. You can have paths to individual
12 # repos, collections of repos in a directory tree, or both.
13 #
12 14 # [paths]
13 15 # virtual/path = /real/path
14 16 # virtual/path = /real/path
17 #
18 # [collections]
19 # /prefix/to/strip/off = /root/of/tree/full/of/repos
20 #
21 # collections example: say directory tree /foo contains repos /foo/bar,
22 # /foo/quux/baz. Give this config section:
23 # [collections]
24 # /foo = /foo
25 # Then repos will list as bar and quux/baz.
15 26
16 27 # Alternatively you can pass a list of ('virtual/path', '/real/path') tuples
17 28 # or use a dictionary with entries like 'virtual/path': '/real/path'
18 29
19 30 h = hgweb.hgwebdir("hgweb.config")
20 31 h.run()
@@ -1,1145 +1,1155
1 1 # hgweb.py - web interface to a mercurial repository
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005 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, cgi, sys, urllib
10 10 from demandload import demandload
11 11 demandload(globals(), "mdiff time re socket zlib errno ui hg ConfigParser")
12 12 demandload(globals(), "zipfile tempfile StringIO tarfile BaseHTTPServer util")
13 13 demandload(globals(), "mimetypes")
14 14 from node import *
15 15 from i18n import gettext as _
16 16
17 17 def templatepath():
18 18 for f in "templates", "../templates":
19 19 p = os.path.join(os.path.dirname(__file__), f)
20 20 if os.path.isdir(p):
21 21 return p
22 22
23 23 def age(x):
24 24 def plural(t, c):
25 25 if c == 1:
26 26 return t
27 27 return t + "s"
28 28 def fmt(t, c):
29 29 return "%d %s" % (c, plural(t, c))
30 30
31 31 now = time.time()
32 32 then = x[0]
33 33 delta = max(1, int(now - then))
34 34
35 35 scales = [["second", 1],
36 36 ["minute", 60],
37 37 ["hour", 3600],
38 38 ["day", 3600 * 24],
39 39 ["week", 3600 * 24 * 7],
40 40 ["month", 3600 * 24 * 30],
41 41 ["year", 3600 * 24 * 365]]
42 42
43 43 scales.reverse()
44 44
45 45 for t, s in scales:
46 46 n = delta / s
47 47 if n >= 2 or s == 1:
48 48 return fmt(t, n)
49 49
50 50 def nl2br(text):
51 51 return text.replace('\n', '<br/>\n')
52 52
53 53 def obfuscate(text):
54 54 return ''.join(['&#%d;' % ord(c) for c in text])
55 55
56 56 def up(p):
57 57 if p[0] != "/":
58 58 p = "/" + p
59 59 if p[-1] == "/":
60 60 p = p[:-1]
61 61 up = os.path.dirname(p)
62 62 if up == "/":
63 63 return "/"
64 64 return up + "/"
65 65
66 66 def get_mtime(repo_path):
67 67 hg_path = os.path.join(repo_path, ".hg")
68 68 cl_path = os.path.join(hg_path, "00changelog.i")
69 69 if os.path.exists(os.path.join(cl_path)):
70 70 return os.stat(cl_path).st_mtime
71 71 else:
72 72 return os.stat(hg_path).st_mtime
73 73
74 74 class hgrequest(object):
75 75 def __init__(self, inp=None, out=None, env=None):
76 76 self.inp = inp or sys.stdin
77 77 self.out = out or sys.stdout
78 78 self.env = env or os.environ
79 79 self.form = cgi.parse(self.inp, self.env, keep_blank_values=1)
80 80
81 81 def write(self, *things):
82 82 for thing in things:
83 83 if hasattr(thing, "__iter__"):
84 84 for part in thing:
85 85 self.write(part)
86 86 else:
87 87 try:
88 88 self.out.write(str(thing))
89 89 except socket.error, inst:
90 90 if inst[0] != errno.ECONNRESET:
91 91 raise
92 92
93 93 def header(self, headers=[('Content-type','text/html')]):
94 94 for header in headers:
95 95 self.out.write("%s: %s\r\n" % header)
96 96 self.out.write("\r\n")
97 97
98 98 def httphdr(self, type, file="", size=0):
99 99
100 100 headers = [('Content-type', type)]
101 101 if file:
102 102 headers.append(('Content-disposition', 'attachment; filename=%s' % file))
103 103 if size > 0:
104 104 headers.append(('Content-length', str(size)))
105 105 self.header(headers)
106 106
107 107 class templater(object):
108 108 def __init__(self, mapfile, filters={}, defaults={}):
109 109 self.cache = {}
110 110 self.map = {}
111 111 self.base = os.path.dirname(mapfile)
112 112 self.filters = filters
113 113 self.defaults = defaults
114 114
115 115 for l in file(mapfile):
116 116 m = re.match(r'(\S+)\s*=\s*"(.*)"$', l)
117 117 if m:
118 118 self.cache[m.group(1)] = m.group(2)
119 119 else:
120 120 m = re.match(r'(\S+)\s*=\s*(\S+)', l)
121 121 if m:
122 122 self.map[m.group(1)] = os.path.join(self.base, m.group(2))
123 123 else:
124 124 raise LookupError(_("unknown map entry '%s'") % l)
125 125
126 126 def __call__(self, t, **map):
127 127 m = self.defaults.copy()
128 128 m.update(map)
129 129 try:
130 130 tmpl = self.cache[t]
131 131 except KeyError:
132 132 tmpl = self.cache[t] = file(self.map[t]).read()
133 133 return self.template(tmpl, self.filters, **m)
134 134
135 135 def template(self, tmpl, filters={}, **map):
136 136 while tmpl:
137 137 m = re.search(r"#([a-zA-Z0-9]+)((%[a-zA-Z0-9]+)*)((\|[a-zA-Z0-9]+)*)#", tmpl)
138 138 if m:
139 139 yield tmpl[:m.start(0)]
140 140 v = map.get(m.group(1), "")
141 141 v = callable(v) and v(**map) or v
142 142
143 143 format = m.group(2)
144 144 fl = m.group(4)
145 145
146 146 if format:
147 147 q = v.__iter__
148 148 for i in q():
149 149 lm = map.copy()
150 150 lm.update(i)
151 151 yield self(format[1:], **lm)
152 152
153 153 v = ""
154 154
155 155 elif fl:
156 156 for f in fl.split("|")[1:]:
157 157 v = filters[f](v)
158 158
159 159 yield v
160 160 tmpl = tmpl[m.end(0):]
161 161 else:
162 162 yield tmpl
163 163 return
164 164
165 165 common_filters = {
166 166 "escape": lambda x: cgi.escape(x, True),
167 167 "urlescape": urllib.quote,
168 168 "strip": lambda x: x.strip(),
169 169 "age": age,
170 170 "date": lambda x: util.datestr(x),
171 171 "addbreaks": nl2br,
172 172 "obfuscate": obfuscate,
173 173 "short": (lambda x: x[:12]),
174 174 "firstline": (lambda x: x.splitlines(1)[0]),
175 175 "permissions": (lambda x: x and "-rwxr-xr-x" or "-rw-r--r--"),
176 176 "rfc822date": lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S"),
177 177 }
178 178
179 179 class hgweb(object):
180 180 def __init__(self, repo, name=None):
181 181 if type(repo) == type(""):
182 182 self.repo = hg.repository(ui.ui(), repo)
183 183 else:
184 184 self.repo = repo
185 185
186 186 self.mtime = -1
187 187 self.reponame = name
188 188 self.archives = 'zip', 'gz', 'bz2'
189 189
190 190 def refresh(self):
191 191 mtime = get_mtime(self.repo.root)
192 192 if mtime != self.mtime:
193 193 self.mtime = mtime
194 194 self.repo = hg.repository(self.repo.ui, self.repo.root)
195 195 self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10))
196 196 self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10))
197 197 self.allowpull = self.repo.ui.configbool("web", "allowpull", True)
198 198
199 199 def archivelist(self, nodeid):
200 200 for i in self.archives:
201 201 if self.repo.ui.configbool("web", "allow" + i, False):
202 202 yield {"type" : i, "node" : nodeid}
203 203
204 204 def listfiles(self, files, mf):
205 205 for f in files[:self.maxfiles]:
206 206 yield self.t("filenodelink", node=hex(mf[f]), file=f)
207 207 if len(files) > self.maxfiles:
208 208 yield self.t("fileellipses")
209 209
210 210 def listfilediffs(self, files, changeset):
211 211 for f in files[:self.maxfiles]:
212 212 yield self.t("filedifflink", node=hex(changeset), file=f)
213 213 if len(files) > self.maxfiles:
214 214 yield self.t("fileellipses")
215 215
216 216 def siblings(self, siblings=[], rev=None, hiderev=None, **args):
217 217 if not rev:
218 218 rev = lambda x: ""
219 219 siblings = [s for s in siblings if s != nullid]
220 220 if len(siblings) == 1 and rev(siblings[0]) == hiderev:
221 221 return
222 222 for s in siblings:
223 223 yield dict(node=hex(s), rev=rev(s), **args)
224 224
225 225 def renamelink(self, fl, node):
226 226 r = fl.renamed(node)
227 227 if r:
228 228 return [dict(file=r[0], node=hex(r[1]))]
229 229 return []
230 230
231 231 def showtag(self, t1, node=nullid, **args):
232 232 for t in self.repo.nodetags(node):
233 233 yield self.t(t1, tag=t, **args)
234 234
235 235 def diff(self, node1, node2, files):
236 236 def filterfiles(filters, files):
237 237 l = [x for x in files if x in filters]
238 238
239 239 for t in filters:
240 240 if t and t[-1] != os.sep:
241 241 t += os.sep
242 242 l += [x for x in files if x.startswith(t)]
243 243 return l
244 244
245 245 parity = [0]
246 246 def diffblock(diff, f, fn):
247 247 yield self.t("diffblock",
248 248 lines=prettyprintlines(diff),
249 249 parity=parity[0],
250 250 file=f,
251 251 filenode=hex(fn or nullid))
252 252 parity[0] = 1 - parity[0]
253 253
254 254 def prettyprintlines(diff):
255 255 for l in diff.splitlines(1):
256 256 if l.startswith('+'):
257 257 yield self.t("difflineplus", line=l)
258 258 elif l.startswith('-'):
259 259 yield self.t("difflineminus", line=l)
260 260 elif l.startswith('@'):
261 261 yield self.t("difflineat", line=l)
262 262 else:
263 263 yield self.t("diffline", line=l)
264 264
265 265 r = self.repo
266 266 cl = r.changelog
267 267 mf = r.manifest
268 268 change1 = cl.read(node1)
269 269 change2 = cl.read(node2)
270 270 mmap1 = mf.read(change1[0])
271 271 mmap2 = mf.read(change2[0])
272 272 date1 = util.datestr(change1[2])
273 273 date2 = util.datestr(change2[2])
274 274
275 275 modified, added, removed, deleted, unknown = r.changes(node1, node2)
276 276 if files:
277 277 modified, added, removed = map(lambda x: filterfiles(files, x),
278 278 (modified, added, removed))
279 279
280 280 diffopts = self.repo.ui.diffopts()
281 281 showfunc = diffopts['showfunc']
282 282 ignorews = diffopts['ignorews']
283 283 for f in modified:
284 284 to = r.file(f).read(mmap1[f])
285 285 tn = r.file(f).read(mmap2[f])
286 286 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
287 287 showfunc=showfunc, ignorews=ignorews), f, tn)
288 288 for f in added:
289 289 to = None
290 290 tn = r.file(f).read(mmap2[f])
291 291 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
292 292 showfunc=showfunc, ignorews=ignorews), f, tn)
293 293 for f in removed:
294 294 to = r.file(f).read(mmap1[f])
295 295 tn = None
296 296 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
297 297 showfunc=showfunc, ignorews=ignorews), f, tn)
298 298
299 299 def changelog(self, pos):
300 300 def changenav(**map):
301 301 def seq(factor, maxchanges=None):
302 302 if maxchanges:
303 303 yield maxchanges
304 304 if maxchanges >= 20 and maxchanges <= 40:
305 305 yield 50
306 306 else:
307 307 yield 1 * factor
308 308 yield 3 * factor
309 309 for f in seq(factor * 10):
310 310 yield f
311 311
312 312 l = []
313 313 last = 0
314 314 for f in seq(1, self.maxchanges):
315 315 if f < self.maxchanges or f <= last:
316 316 continue
317 317 if f > count:
318 318 break
319 319 last = f
320 320 r = "%d" % f
321 321 if pos + f < count:
322 322 l.append(("+" + r, pos + f))
323 323 if pos - f >= 0:
324 324 l.insert(0, ("-" + r, pos - f))
325 325
326 326 yield {"rev": 0, "label": "(0)"}
327 327
328 328 for label, rev in l:
329 329 yield {"label": label, "rev": rev}
330 330
331 331 yield {"label": "tip", "rev": "tip"}
332 332
333 333 def changelist(**map):
334 334 parity = (start - end) & 1
335 335 cl = self.repo.changelog
336 336 l = [] # build a list in forward order for efficiency
337 337 for i in range(start, end):
338 338 n = cl.node(i)
339 339 changes = cl.read(n)
340 340 hn = hex(n)
341 341
342 342 l.insert(0, {"parity": parity,
343 343 "author": changes[1],
344 344 "parent": self.siblings(cl.parents(n), cl.rev,
345 345 cl.rev(n) - 1),
346 346 "child": self.siblings(cl.children(n), cl.rev,
347 347 cl.rev(n) + 1),
348 348 "changelogtag": self.showtag("changelogtag",n),
349 349 "manifest": hex(changes[0]),
350 350 "desc": changes[4],
351 351 "date": changes[2],
352 352 "files": self.listfilediffs(changes[3], n),
353 353 "rev": i,
354 354 "node": hn})
355 355 parity = 1 - parity
356 356
357 357 for e in l:
358 358 yield e
359 359
360 360 cl = self.repo.changelog
361 361 mf = cl.read(cl.tip())[0]
362 362 count = cl.count()
363 363 start = max(0, pos - self.maxchanges + 1)
364 364 end = min(count, start + self.maxchanges)
365 365 pos = end - 1
366 366
367 367 yield self.t('changelog',
368 368 changenav=changenav,
369 369 manifest=hex(mf),
370 370 rev=pos, changesets=count, entries=changelist)
371 371
372 372 def search(self, query):
373 373
374 374 def changelist(**map):
375 375 cl = self.repo.changelog
376 376 count = 0
377 377 qw = query.lower().split()
378 378
379 379 def revgen():
380 380 for i in range(cl.count() - 1, 0, -100):
381 381 l = []
382 382 for j in range(max(0, i - 100), i):
383 383 n = cl.node(j)
384 384 changes = cl.read(n)
385 385 l.append((n, j, changes))
386 386 l.reverse()
387 387 for e in l:
388 388 yield e
389 389
390 390 for n, i, changes in revgen():
391 391 miss = 0
392 392 for q in qw:
393 393 if not (q in changes[1].lower() or
394 394 q in changes[4].lower() or
395 395 q in " ".join(changes[3][:20]).lower()):
396 396 miss = 1
397 397 break
398 398 if miss:
399 399 continue
400 400
401 401 count += 1
402 402 hn = hex(n)
403 403
404 404 yield self.t('searchentry',
405 405 parity=count & 1,
406 406 author=changes[1],
407 407 parent=self.siblings(cl.parents(n), cl.rev),
408 408 child=self.siblings(cl.children(n), cl.rev),
409 409 changelogtag=self.showtag("changelogtag",n),
410 410 manifest=hex(changes[0]),
411 411 desc=changes[4],
412 412 date=changes[2],
413 413 files=self.listfilediffs(changes[3], n),
414 414 rev=i,
415 415 node=hn)
416 416
417 417 if count >= self.maxchanges:
418 418 break
419 419
420 420 cl = self.repo.changelog
421 421 mf = cl.read(cl.tip())[0]
422 422
423 423 yield self.t('search',
424 424 query=query,
425 425 manifest=hex(mf),
426 426 entries=changelist)
427 427
428 428 def changeset(self, nodeid):
429 429 cl = self.repo.changelog
430 430 n = self.repo.lookup(nodeid)
431 431 nodeid = hex(n)
432 432 changes = cl.read(n)
433 433 p1 = cl.parents(n)[0]
434 434
435 435 files = []
436 436 mf = self.repo.manifest.read(changes[0])
437 437 for f in changes[3]:
438 438 files.append(self.t("filenodelink",
439 439 filenode=hex(mf.get(f, nullid)), file=f))
440 440
441 441 def diff(**map):
442 442 yield self.diff(p1, n, None)
443 443
444 444 yield self.t('changeset',
445 445 diff=diff,
446 446 rev=cl.rev(n),
447 447 node=nodeid,
448 448 parent=self.siblings(cl.parents(n), cl.rev),
449 449 child=self.siblings(cl.children(n), cl.rev),
450 450 changesettag=self.showtag("changesettag",n),
451 451 manifest=hex(changes[0]),
452 452 author=changes[1],
453 453 desc=changes[4],
454 454 date=changes[2],
455 455 files=files,
456 456 archives=self.archivelist(nodeid))
457 457
458 458 def filelog(self, f, filenode):
459 459 cl = self.repo.changelog
460 460 fl = self.repo.file(f)
461 461 filenode = hex(fl.lookup(filenode))
462 462 count = fl.count()
463 463
464 464 def entries(**map):
465 465 l = []
466 466 parity = (count - 1) & 1
467 467
468 468 for i in range(count):
469 469 n = fl.node(i)
470 470 lr = fl.linkrev(n)
471 471 cn = cl.node(lr)
472 472 cs = cl.read(cl.node(lr))
473 473
474 474 l.insert(0, {"parity": parity,
475 475 "filenode": hex(n),
476 476 "filerev": i,
477 477 "file": f,
478 478 "node": hex(cn),
479 479 "author": cs[1],
480 480 "date": cs[2],
481 481 "rename": self.renamelink(fl, n),
482 482 "parent": self.siblings(fl.parents(n),
483 483 fl.rev, file=f),
484 484 "child": self.siblings(fl.children(n),
485 485 fl.rev, file=f),
486 486 "desc": cs[4]})
487 487 parity = 1 - parity
488 488
489 489 for e in l:
490 490 yield e
491 491
492 492 yield self.t("filelog", file=f, filenode=filenode, entries=entries)
493 493
494 494 def filerevision(self, f, node):
495 495 fl = self.repo.file(f)
496 496 n = fl.lookup(node)
497 497 node = hex(n)
498 498 text = fl.read(n)
499 499 changerev = fl.linkrev(n)
500 500 cl = self.repo.changelog
501 501 cn = cl.node(changerev)
502 502 cs = cl.read(cn)
503 503 mfn = cs[0]
504 504
505 505 mt = mimetypes.guess_type(f)[0]
506 506 rawtext = text
507 507 if util.binary(text):
508 508 text = "(binary:%s)" % mt
509 509
510 510 def lines():
511 511 for l, t in enumerate(text.splitlines(1)):
512 512 yield {"line": t,
513 513 "linenumber": "% 6d" % (l + 1),
514 514 "parity": l & 1}
515 515
516 516 yield self.t("filerevision",
517 517 file=f,
518 518 filenode=node,
519 519 path=up(f),
520 520 text=lines(),
521 521 raw=rawtext,
522 522 mimetype=mt,
523 523 rev=changerev,
524 524 node=hex(cn),
525 525 manifest=hex(mfn),
526 526 author=cs[1],
527 527 date=cs[2],
528 528 parent=self.siblings(fl.parents(n), fl.rev, file=f),
529 529 child=self.siblings(fl.children(n), fl.rev, file=f),
530 530 rename=self.renamelink(fl, n),
531 531 permissions=self.repo.manifest.readflags(mfn)[f])
532 532
533 533 def fileannotate(self, f, node):
534 534 bcache = {}
535 535 ncache = {}
536 536 fl = self.repo.file(f)
537 537 n = fl.lookup(node)
538 538 node = hex(n)
539 539 changerev = fl.linkrev(n)
540 540
541 541 cl = self.repo.changelog
542 542 cn = cl.node(changerev)
543 543 cs = cl.read(cn)
544 544 mfn = cs[0]
545 545
546 546 def annotate(**map):
547 547 parity = 1
548 548 last = None
549 549 for r, l in fl.annotate(n):
550 550 try:
551 551 cnode = ncache[r]
552 552 except KeyError:
553 553 cnode = ncache[r] = self.repo.changelog.node(r)
554 554
555 555 try:
556 556 name = bcache[r]
557 557 except KeyError:
558 558 cl = self.repo.changelog.read(cnode)
559 559 bcache[r] = name = self.repo.ui.shortuser(cl[1])
560 560
561 561 if last != cnode:
562 562 parity = 1 - parity
563 563 last = cnode
564 564
565 565 yield {"parity": parity,
566 566 "node": hex(cnode),
567 567 "rev": r,
568 568 "author": name,
569 569 "file": f,
570 570 "line": l}
571 571
572 572 yield self.t("fileannotate",
573 573 file=f,
574 574 filenode=node,
575 575 annotate=annotate,
576 576 path=up(f),
577 577 rev=changerev,
578 578 node=hex(cn),
579 579 manifest=hex(mfn),
580 580 author=cs[1],
581 581 date=cs[2],
582 582 rename=self.renamelink(fl, n),
583 583 parent=self.siblings(fl.parents(n), fl.rev, file=f),
584 584 child=self.siblings(fl.children(n), fl.rev, file=f),
585 585 permissions=self.repo.manifest.readflags(mfn)[f])
586 586
587 587 def manifest(self, mnode, path):
588 588 man = self.repo.manifest
589 589 mn = man.lookup(mnode)
590 590 mnode = hex(mn)
591 591 mf = man.read(mn)
592 592 rev = man.rev(mn)
593 593 node = self.repo.changelog.node(rev)
594 594 mff = man.readflags(mn)
595 595
596 596 files = {}
597 597
598 598 p = path[1:]
599 599 if p and p[-1] != "/":
600 600 p += "/"
601 601 l = len(p)
602 602
603 603 for f,n in mf.items():
604 604 if f[:l] != p:
605 605 continue
606 606 remain = f[l:]
607 607 if "/" in remain:
608 608 short = remain[:remain.find("/") + 1] # bleah
609 609 files[short] = (f, None)
610 610 else:
611 611 short = os.path.basename(remain)
612 612 files[short] = (f, n)
613 613
614 614 def filelist(**map):
615 615 parity = 0
616 616 fl = files.keys()
617 617 fl.sort()
618 618 for f in fl:
619 619 full, fnode = files[f]
620 620 if not fnode:
621 621 continue
622 622
623 623 yield {"file": full,
624 624 "manifest": mnode,
625 625 "filenode": hex(fnode),
626 626 "parity": parity,
627 627 "basename": f,
628 628 "permissions": mff[full]}
629 629 parity = 1 - parity
630 630
631 631 def dirlist(**map):
632 632 parity = 0
633 633 fl = files.keys()
634 634 fl.sort()
635 635 for f in fl:
636 636 full, fnode = files[f]
637 637 if fnode:
638 638 continue
639 639
640 640 yield {"parity": parity,
641 641 "path": os.path.join(path, f),
642 642 "manifest": mnode,
643 643 "basename": f[:-1]}
644 644 parity = 1 - parity
645 645
646 646 yield self.t("manifest",
647 647 manifest=mnode,
648 648 rev=rev,
649 649 node=hex(node),
650 650 path=path,
651 651 up=up(path),
652 652 fentries=filelist,
653 653 dentries=dirlist,
654 654 archives=self.archivelist(hex(node)))
655 655
656 656 def tags(self):
657 657 cl = self.repo.changelog
658 658 mf = cl.read(cl.tip())[0]
659 659
660 660 i = self.repo.tagslist()
661 661 i.reverse()
662 662
663 663 def entries(**map):
664 664 parity = 0
665 665 for k,n in i:
666 666 yield {"parity": parity,
667 667 "tag": k,
668 668 "tagmanifest": hex(cl.read(n)[0]),
669 669 "date": cl.read(n)[2],
670 670 "node": hex(n)}
671 671 parity = 1 - parity
672 672
673 673 yield self.t("tags",
674 674 manifest=hex(mf),
675 675 entries=entries)
676 676
677 677 def summary(self):
678 678 cl = self.repo.changelog
679 679 mf = cl.read(cl.tip())[0]
680 680
681 681 i = self.repo.tagslist()
682 682 i.reverse()
683 683
684 684 def tagentries(**map):
685 685 parity = 0
686 686 count = 0
687 687 for k,n in i:
688 688 if k == "tip": # skip tip
689 689 continue;
690 690
691 691 count += 1
692 692 if count > 10: # limit to 10 tags
693 693 break;
694 694
695 695 c = cl.read(n)
696 696 m = c[0]
697 697 t = c[2]
698 698
699 699 yield self.t("tagentry",
700 700 parity = parity,
701 701 tag = k,
702 702 node = hex(n),
703 703 date = t,
704 704 tagmanifest = hex(m))
705 705 parity = 1 - parity
706 706
707 707 def changelist(**map):
708 708 parity = 0
709 709 cl = self.repo.changelog
710 710 l = [] # build a list in forward order for efficiency
711 711 for i in range(start, end):
712 712 n = cl.node(i)
713 713 changes = cl.read(n)
714 714 hn = hex(n)
715 715 t = changes[2]
716 716
717 717 l.insert(0, self.t(
718 718 'shortlogentry',
719 719 parity = parity,
720 720 author = changes[1],
721 721 manifest = hex(changes[0]),
722 722 desc = changes[4],
723 723 date = t,
724 724 rev = i,
725 725 node = hn))
726 726 parity = 1 - parity
727 727
728 728 yield l
729 729
730 730 cl = self.repo.changelog
731 731 mf = cl.read(cl.tip())[0]
732 732 count = cl.count()
733 733 start = max(0, count - self.maxchanges)
734 734 end = min(count, start + self.maxchanges)
735 735 pos = end - 1
736 736
737 737 yield self.t("summary",
738 738 desc = self.repo.ui.config("web", "description", "unknown"),
739 739 owner = (self.repo.ui.config("ui", "username") or # preferred
740 740 self.repo.ui.config("web", "contact") or # deprecated
741 741 self.repo.ui.config("web", "author", "unknown")), # also
742 742 lastchange = (0, 0), # FIXME
743 743 manifest = hex(mf),
744 744 tags = tagentries,
745 745 shortlog = changelist)
746 746
747 747 def filediff(self, file, changeset):
748 748 cl = self.repo.changelog
749 749 n = self.repo.lookup(changeset)
750 750 changeset = hex(n)
751 751 p1 = cl.parents(n)[0]
752 752 cs = cl.read(n)
753 753 mf = self.repo.manifest.read(cs[0])
754 754
755 755 def diff(**map):
756 756 yield self.diff(p1, n, file)
757 757
758 758 yield self.t("filediff",
759 759 file=file,
760 760 filenode=hex(mf.get(file, nullid)),
761 761 node=changeset,
762 762 rev=self.repo.changelog.rev(n),
763 763 parent=self.siblings(cl.parents(n), cl.rev),
764 764 child=self.siblings(cl.children(n), cl.rev),
765 765 diff=diff)
766 766
767 767 def archive(self, req, cnode, type):
768 768 cs = self.repo.changelog.read(cnode)
769 769 mnode = cs[0]
770 770 mf = self.repo.manifest.read(mnode)
771 771 rev = self.repo.manifest.rev(mnode)
772 772 reponame = re.sub(r"\W+", "-", self.reponame)
773 773 name = "%s-%s/" % (reponame, short(cnode))
774 774
775 775 files = mf.keys()
776 776 files.sort()
777 777
778 778 if type == 'zip':
779 779 tmp = tempfile.mkstemp()[1]
780 780 try:
781 781 zf = zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED)
782 782
783 783 for f in files:
784 784 zf.writestr(name + f, self.repo.file(f).read(mf[f]))
785 785 zf.close()
786 786
787 787 f = open(tmp, 'r')
788 788 req.httphdr('application/zip', name[:-1] + '.zip',
789 789 os.path.getsize(tmp))
790 790 req.write(f.read())
791 791 f.close()
792 792 finally:
793 793 os.unlink(tmp)
794 794
795 795 else:
796 796 tf = tarfile.TarFile.open(mode='w|' + type, fileobj=req.out)
797 797 mff = self.repo.manifest.readflags(mnode)
798 798 mtime = int(time.time())
799 799
800 800 if type == "gz":
801 801 encoding = "gzip"
802 802 else:
803 803 encoding = "x-bzip2"
804 804 req.header([('Content-type', 'application/x-tar'),
805 805 ('Content-disposition', 'attachment; filename=%s%s%s' %
806 806 (name[:-1], '.tar.', type)),
807 807 ('Content-encoding', encoding)])
808 808 for fname in files:
809 809 rcont = self.repo.file(fname).read(mf[fname])
810 810 finfo = tarfile.TarInfo(name + fname)
811 811 finfo.mtime = mtime
812 812 finfo.size = len(rcont)
813 813 finfo.mode = mff[fname] and 0755 or 0644
814 814 tf.addfile(finfo, StringIO.StringIO(rcont))
815 815 tf.close()
816 816
817 817 # add tags to things
818 818 # tags -> list of changesets corresponding to tags
819 819 # find tag, changeset, file
820 820
821 821 def run(self, req=hgrequest()):
822 822 def clean(path):
823 823 p = os.path.normpath(path)
824 824 if p[:2] == "..":
825 825 raise "suspicious path"
826 826 return p
827 827
828 828 def header(**map):
829 829 yield self.t("header", **map)
830 830
831 831 def footer(**map):
832 832 yield self.t("footer", **map)
833 833
834 834 def expand_form(form):
835 835 shortcuts = {
836 836 'cl': [('cmd', ['changelog']), ('rev', None)],
837 837 'cs': [('cmd', ['changeset']), ('node', None)],
838 838 'f': [('cmd', ['file']), ('filenode', None)],
839 839 'fl': [('cmd', ['filelog']), ('filenode', None)],
840 840 'fd': [('cmd', ['filediff']), ('node', None)],
841 841 'fa': [('cmd', ['annotate']), ('filenode', None)],
842 842 'mf': [('cmd', ['manifest']), ('manifest', None)],
843 843 'ca': [('cmd', ['archive']), ('node', None)],
844 844 'tags': [('cmd', ['tags'])],
845 845 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
846 846 }
847 847
848 848 for k in shortcuts.iterkeys():
849 849 if form.has_key(k):
850 850 for name, value in shortcuts[k]:
851 851 if value is None:
852 852 value = form[k]
853 853 form[name] = value
854 854 del form[k]
855 855
856 856 self.refresh()
857 857
858 858 expand_form(req.form)
859 859
860 860 t = self.repo.ui.config("web", "templates", templatepath())
861 861 m = os.path.join(t, "map")
862 862 style = self.repo.ui.config("web", "style", "")
863 863 if req.form.has_key('style'):
864 864 style = req.form['style'][0]
865 865 if style:
866 866 b = os.path.basename("map-" + style)
867 867 p = os.path.join(t, b)
868 868 if os.path.isfile(p):
869 869 m = p
870 870
871 871 port = req.env["SERVER_PORT"]
872 872 port = port != "80" and (":" + port) or ""
873 873 uri = req.env["REQUEST_URI"]
874 874 if "?" in uri:
875 875 uri = uri.split("?")[0]
876 876 url = "http://%s%s%s" % (req.env["SERVER_NAME"], port, uri)
877 877 if not self.reponame:
878 878 self.reponame = (self.repo.ui.config("web", "name")
879 879 or uri.strip('/') or self.repo.root)
880 880
881 881 self.t = templater(m, common_filters,
882 882 {"url": url,
883 883 "repo": self.reponame,
884 884 "header": header,
885 885 "footer": footer,
886 886 })
887 887
888 888 if not req.form.has_key('cmd'):
889 889 req.form['cmd'] = [self.t.cache['default'],]
890 890
891 891 if req.form['cmd'][0] == 'changelog':
892 892 c = self.repo.changelog.count() - 1
893 893 hi = c
894 894 if req.form.has_key('rev'):
895 895 hi = req.form['rev'][0]
896 896 try:
897 897 hi = self.repo.changelog.rev(self.repo.lookup(hi))
898 898 except hg.RepoError:
899 899 req.write(self.search(hi))
900 900 return
901 901
902 902 req.write(self.changelog(hi))
903 903
904 904 elif req.form['cmd'][0] == 'changeset':
905 905 req.write(self.changeset(req.form['node'][0]))
906 906
907 907 elif req.form['cmd'][0] == 'manifest':
908 908 req.write(self.manifest(req.form['manifest'][0],
909 909 clean(req.form['path'][0])))
910 910
911 911 elif req.form['cmd'][0] == 'tags':
912 912 req.write(self.tags())
913 913
914 914 elif req.form['cmd'][0] == 'summary':
915 915 req.write(self.summary())
916 916
917 917 elif req.form['cmd'][0] == 'filediff':
918 918 req.write(self.filediff(clean(req.form['file'][0]),
919 919 req.form['node'][0]))
920 920
921 921 elif req.form['cmd'][0] == 'file':
922 922 req.write(self.filerevision(clean(req.form['file'][0]),
923 923 req.form['filenode'][0]))
924 924
925 925 elif req.form['cmd'][0] == 'annotate':
926 926 req.write(self.fileannotate(clean(req.form['file'][0]),
927 927 req.form['filenode'][0]))
928 928
929 929 elif req.form['cmd'][0] == 'filelog':
930 930 req.write(self.filelog(clean(req.form['file'][0]),
931 931 req.form['filenode'][0]))
932 932
933 933 elif req.form['cmd'][0] == 'heads':
934 934 req.httphdr("application/mercurial-0.1")
935 935 h = self.repo.heads()
936 936 req.write(" ".join(map(hex, h)) + "\n")
937 937
938 938 elif req.form['cmd'][0] == 'branches':
939 939 req.httphdr("application/mercurial-0.1")
940 940 nodes = []
941 941 if req.form.has_key('nodes'):
942 942 nodes = map(bin, req.form['nodes'][0].split(" "))
943 943 for b in self.repo.branches(nodes):
944 944 req.write(" ".join(map(hex, b)) + "\n")
945 945
946 946 elif req.form['cmd'][0] == 'between':
947 947 req.httphdr("application/mercurial-0.1")
948 948 nodes = []
949 949 if req.form.has_key('pairs'):
950 950 pairs = [map(bin, p.split("-"))
951 951 for p in req.form['pairs'][0].split(" ")]
952 952 for b in self.repo.between(pairs):
953 953 req.write(" ".join(map(hex, b)) + "\n")
954 954
955 955 elif req.form['cmd'][0] == 'changegroup':
956 956 req.httphdr("application/mercurial-0.1")
957 957 nodes = []
958 958 if not self.allowpull:
959 959 return
960 960
961 961 if req.form.has_key('roots'):
962 962 nodes = map(bin, req.form['roots'][0].split(" "))
963 963
964 964 z = zlib.compressobj()
965 965 f = self.repo.changegroup(nodes)
966 966 while 1:
967 967 chunk = f.read(4096)
968 968 if not chunk:
969 969 break
970 970 req.write(z.compress(chunk))
971 971
972 972 req.write(z.flush())
973 973
974 974 elif req.form['cmd'][0] == 'archive':
975 975 changeset = self.repo.lookup(req.form['node'][0])
976 976 type = req.form['type'][0]
977 977 if (type in self.archives and
978 978 self.repo.ui.configbool("web", "allow" + type, False)):
979 979 self.archive(req, changeset, type)
980 980 return
981 981
982 982 req.write(self.t("error"))
983 983
984 984 else:
985 985 req.write(self.t("error"))
986 986
987 987 def create_server(repo):
988 988
989 989 def openlog(opt, default):
990 990 if opt and opt != '-':
991 991 return open(opt, 'w')
992 992 return default
993 993
994 994 address = repo.ui.config("web", "address", "")
995 995 port = int(repo.ui.config("web", "port", 8000))
996 996 use_ipv6 = repo.ui.configbool("web", "ipv6")
997 997 accesslog = openlog(repo.ui.config("web", "accesslog", "-"), sys.stdout)
998 998 errorlog = openlog(repo.ui.config("web", "errorlog", "-"), sys.stderr)
999 999
1000 1000 class IPv6HTTPServer(BaseHTTPServer.HTTPServer):
1001 1001 address_family = getattr(socket, 'AF_INET6', None)
1002 1002
1003 1003 def __init__(self, *args, **kwargs):
1004 1004 if self.address_family is None:
1005 1005 raise hg.RepoError(_('IPv6 not available on this system'))
1006 1006 BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs)
1007 1007
1008 1008 class hgwebhandler(BaseHTTPServer.BaseHTTPRequestHandler):
1009 1009 def log_error(self, format, *args):
1010 1010 errorlog.write("%s - - [%s] %s\n" % (self.address_string(),
1011 1011 self.log_date_time_string(),
1012 1012 format % args))
1013 1013
1014 1014 def log_message(self, format, *args):
1015 1015 accesslog.write("%s - - [%s] %s\n" % (self.address_string(),
1016 1016 self.log_date_time_string(),
1017 1017 format % args))
1018 1018
1019 1019 def do_POST(self):
1020 1020 try:
1021 1021 self.do_hgweb()
1022 1022 except socket.error, inst:
1023 1023 if inst[0] != errno.EPIPE:
1024 1024 raise
1025 1025
1026 1026 def do_GET(self):
1027 1027 self.do_POST()
1028 1028
1029 1029 def do_hgweb(self):
1030 1030 query = ""
1031 1031 p = self.path.find("?")
1032 1032 if p:
1033 1033 query = self.path[p + 1:]
1034 1034 query = query.replace('+', ' ')
1035 1035
1036 1036 env = {}
1037 1037 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
1038 1038 env['REQUEST_METHOD'] = self.command
1039 1039 env['SERVER_NAME'] = self.server.server_name
1040 1040 env['SERVER_PORT'] = str(self.server.server_port)
1041 1041 env['REQUEST_URI'] = "/"
1042 1042 if query:
1043 1043 env['QUERY_STRING'] = query
1044 1044 host = self.address_string()
1045 1045 if host != self.client_address[0]:
1046 1046 env['REMOTE_HOST'] = host
1047 1047 env['REMOTE_ADDR'] = self.client_address[0]
1048 1048
1049 1049 if self.headers.typeheader is None:
1050 1050 env['CONTENT_TYPE'] = self.headers.type
1051 1051 else:
1052 1052 env['CONTENT_TYPE'] = self.headers.typeheader
1053 1053 length = self.headers.getheader('content-length')
1054 1054 if length:
1055 1055 env['CONTENT_LENGTH'] = length
1056 1056 accept = []
1057 1057 for line in self.headers.getallmatchingheaders('accept'):
1058 1058 if line[:1] in "\t\n\r ":
1059 1059 accept.append(line.strip())
1060 1060 else:
1061 1061 accept = accept + line[7:].split(',')
1062 1062 env['HTTP_ACCEPT'] = ','.join(accept)
1063 1063
1064 1064 req = hgrequest(self.rfile, self.wfile, env)
1065 1065 self.send_response(200, "Script output follows")
1066 1066 hg.run(req)
1067 1067
1068 1068 hg = hgweb(repo)
1069 1069 if use_ipv6:
1070 1070 return IPv6HTTPServer((address, port), hgwebhandler)
1071 1071 else:
1072 1072 return BaseHTTPServer.HTTPServer((address, port), hgwebhandler)
1073 1073
1074 1074 # This is a stopgap
1075 1075 class hgwebdir(object):
1076 1076 def __init__(self, config):
1077 1077 def cleannames(items):
1078 return [(name.strip('/'), path) for name, path in items]
1078 return [(name.strip(os.sep), path) for name, path in items]
1079 1079
1080 if type(config) == type([]):
1080 if isinstance(config, (list, tuple)):
1081 1081 self.repos = cleannames(config)
1082 elif type(config) == type({}):
1082 elif isinstance(config, dict):
1083 1083 self.repos = cleannames(config.items())
1084 1084 self.repos.sort()
1085 1085 else:
1086 1086 cp = ConfigParser.SafeConfigParser()
1087 1087 cp.read(config)
1088 self.repos = cleannames(cp.items("paths"))
1088 self.repos = []
1089 if cp.has_section('paths'):
1090 self.repos.extend(cleannames(cp.items('paths')))
1091 if cp.has_section('collections'):
1092 for prefix, root in cp.items('collections'):
1093 for path in util.walkrepos(root):
1094 repo = os.path.normpath(path)
1095 name = repo
1096 if name.startswith(prefix):
1097 name = name[len(prefix):]
1098 self.repos.append((name.lstrip(os.sep), repo))
1089 1099 self.repos.sort()
1090 1100
1091 1101 def run(self, req=hgrequest()):
1092 1102 def header(**map):
1093 1103 yield tmpl("header", **map)
1094 1104
1095 1105 def footer(**map):
1096 1106 yield tmpl("footer", **map)
1097 1107
1098 1108 m = os.path.join(templatepath(), "map")
1099 1109 tmpl = templater(m, common_filters,
1100 1110 {"header": header, "footer": footer})
1101 1111
1102 1112 def entries(**map):
1103 1113 parity = 0
1104 1114 for name, path in self.repos:
1105 1115 u = ui.ui()
1106 1116 try:
1107 1117 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
1108 1118 except IOError:
1109 1119 pass
1110 1120 get = u.config
1111 1121
1112 1122 url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
1113 1123 .replace("//", "/"))
1114 1124
1115 1125 # update time with local timezone
1116 1126 try:
1117 1127 d = (get_mtime(path), util.makedate()[1])
1118 1128 except OSError:
1119 1129 continue
1120 1130
1121 1131 yield dict(contact=(get("ui", "username") or # preferred
1122 1132 get("web", "contact") or # deprecated
1123 1133 get("web", "author", "unknown")), # also
1124 1134 name=get("web", "name", name),
1125 1135 url=url,
1126 1136 parity=parity,
1127 1137 shortdesc=get("web", "description", "unknown"),
1128 1138 lastupdate=d)
1129 1139
1130 1140 parity = 1 - parity
1131 1141
1132 1142 virtual = req.env.get("PATH_INFO", "").strip('/')
1133 1143 if virtual:
1134 1144 real = dict(self.repos).get(virtual)
1135 1145 if real:
1136 1146 try:
1137 1147 hgweb(real).run(req)
1138 1148 except IOError, inst:
1139 1149 req.write(tmpl("error", error=inst.strerror))
1140 1150 except hg.RepoError, inst:
1141 1151 req.write(tmpl("error", error=str(inst)))
1142 1152 else:
1143 1153 req.write(tmpl("notfound", repo=virtual))
1144 1154 else:
1145 1155 req.write(tmpl("index", entries=entries))
@@ -1,692 +1,705
1 1 """
2 2 util.py - Mercurial utility functions and platform specfic implementations
3 3
4 4 Copyright 2005 K. Thananchayan <thananck@yahoo.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 This contains helper routines that are independent of the SCM core and hide
10 10 platform-specific details from the core.
11 11 """
12 12
13 13 import os, errno
14 14 from i18n import gettext as _
15 15 from demandload import *
16 16 demandload(globals(), "cStringIO errno popen2 re shutil sys tempfile")
17 17 demandload(globals(), "threading time")
18 18
19 19 def pipefilter(s, cmd):
20 20 '''filter string S through command CMD, returning its output'''
21 21 (pout, pin) = popen2.popen2(cmd, -1, 'b')
22 22 def writer():
23 23 pin.write(s)
24 24 pin.close()
25 25
26 26 # we should use select instead on UNIX, but this will work on most
27 27 # systems, including Windows
28 28 w = threading.Thread(target=writer)
29 29 w.start()
30 30 f = pout.read()
31 31 pout.close()
32 32 w.join()
33 33 return f
34 34
35 35 def tempfilter(s, cmd):
36 36 '''filter string S through a pair of temporary files with CMD.
37 37 CMD is used as a template to create the real command to be run,
38 38 with the strings INFILE and OUTFILE replaced by the real names of
39 39 the temporary files generated.'''
40 40 inname, outname = None, None
41 41 try:
42 42 infd, inname = tempfile.mkstemp(prefix='hgfin')
43 43 fp = os.fdopen(infd, 'wb')
44 44 fp.write(s)
45 45 fp.close()
46 46 outfd, outname = tempfile.mkstemp(prefix='hgfout')
47 47 os.close(outfd)
48 48 cmd = cmd.replace('INFILE', inname)
49 49 cmd = cmd.replace('OUTFILE', outname)
50 50 code = os.system(cmd)
51 51 if code: raise Abort(_("command '%s' failed: %s") %
52 52 (cmd, explain_exit(code)))
53 53 return open(outname, 'rb').read()
54 54 finally:
55 55 try:
56 56 if inname: os.unlink(inname)
57 57 except: pass
58 58 try:
59 59 if outname: os.unlink(outname)
60 60 except: pass
61 61
62 62 filtertable = {
63 63 'tempfile:': tempfilter,
64 64 'pipe:': pipefilter,
65 65 }
66 66
67 67 def filter(s, cmd):
68 68 "filter a string through a command that transforms its input to its output"
69 69 for name, fn in filtertable.iteritems():
70 70 if cmd.startswith(name):
71 71 return fn(s, cmd[len(name):].lstrip())
72 72 return pipefilter(s, cmd)
73 73
74 74 def patch(strip, patchname, ui):
75 75 """apply the patch <patchname> to the working directory.
76 76 a list of patched files is returned"""
77 77 fp = os.popen('patch -p%d < "%s"' % (strip, patchname))
78 78 files = {}
79 79 for line in fp:
80 80 line = line.rstrip()
81 81 ui.status("%s\n" % line)
82 82 if line.startswith('patching file '):
83 83 pf = parse_patch_output(line)
84 84 files.setdefault(pf, 1)
85 85 code = fp.close()
86 86 if code:
87 87 raise Abort(_("patch command failed: %s") % explain_exit(code)[0])
88 88 return files.keys()
89 89
90 90 def binary(s):
91 91 """return true if a string is binary data using diff's heuristic"""
92 92 if s and '\0' in s[:4096]:
93 93 return True
94 94 return False
95 95
96 96 def unique(g):
97 97 """return the uniq elements of iterable g"""
98 98 seen = {}
99 99 for f in g:
100 100 if f not in seen:
101 101 seen[f] = 1
102 102 yield f
103 103
104 104 class Abort(Exception):
105 105 """Raised if a command needs to print an error and exit."""
106 106
107 107 def always(fn): return True
108 108 def never(fn): return False
109 109
110 110 def patkind(name, dflt_pat='glob'):
111 111 """Split a string into an optional pattern kind prefix and the
112 112 actual pattern."""
113 113 for prefix in 're', 'glob', 'path', 'relglob', 'relpath', 'relre':
114 114 if name.startswith(prefix + ':'): return name.split(':', 1)
115 115 return dflt_pat, name
116 116
117 117 def globre(pat, head='^', tail='$'):
118 118 "convert a glob pattern into a regexp"
119 119 i, n = 0, len(pat)
120 120 res = ''
121 121 group = False
122 122 def peek(): return i < n and pat[i]
123 123 while i < n:
124 124 c = pat[i]
125 125 i = i+1
126 126 if c == '*':
127 127 if peek() == '*':
128 128 i += 1
129 129 res += '.*'
130 130 else:
131 131 res += '[^/]*'
132 132 elif c == '?':
133 133 res += '.'
134 134 elif c == '[':
135 135 j = i
136 136 if j < n and pat[j] in '!]':
137 137 j += 1
138 138 while j < n and pat[j] != ']':
139 139 j += 1
140 140 if j >= n:
141 141 res += '\\['
142 142 else:
143 143 stuff = pat[i:j].replace('\\','\\\\')
144 144 i = j + 1
145 145 if stuff[0] == '!':
146 146 stuff = '^' + stuff[1:]
147 147 elif stuff[0] == '^':
148 148 stuff = '\\' + stuff
149 149 res = '%s[%s]' % (res, stuff)
150 150 elif c == '{':
151 151 group = True
152 152 res += '(?:'
153 153 elif c == '}' and group:
154 154 res += ')'
155 155 group = False
156 156 elif c == ',' and group:
157 157 res += '|'
158 158 else:
159 159 res += re.escape(c)
160 160 return head + res + tail
161 161
162 162 _globchars = {'[': 1, '{': 1, '*': 1, '?': 1}
163 163
164 164 def pathto(n1, n2):
165 165 '''return the relative path from one place to another.
166 166 this returns a path in the form used by the local filesystem, not hg.'''
167 167 if not n1: return localpath(n2)
168 168 a, b = n1.split('/'), n2.split('/')
169 169 a.reverse()
170 170 b.reverse()
171 171 while a and b and a[-1] == b[-1]:
172 172 a.pop()
173 173 b.pop()
174 174 b.reverse()
175 175 return os.sep.join((['..'] * len(a)) + b)
176 176
177 177 def canonpath(root, cwd, myname):
178 178 """return the canonical path of myname, given cwd and root"""
179 179 if root == os.sep:
180 180 rootsep = os.sep
181 181 else:
182 182 rootsep = root + os.sep
183 183 name = myname
184 184 if not name.startswith(os.sep):
185 185 name = os.path.join(root, cwd, name)
186 186 name = os.path.normpath(name)
187 187 if name.startswith(rootsep):
188 188 return pconvert(name[len(rootsep):])
189 189 elif name == root:
190 190 return ''
191 191 else:
192 192 raise Abort('%s not under root' % myname)
193 193
194 194 def matcher(canonroot, cwd='', names=['.'], inc=[], exc=[], head='', src=None):
195 195 return _matcher(canonroot, cwd, names, inc, exc, head, 'glob', src)
196 196
197 197 def cmdmatcher(canonroot, cwd='', names=['.'], inc=[], exc=[], head='', src=None):
198 198 if os.name == 'nt':
199 199 dflt_pat = 'glob'
200 200 else:
201 201 dflt_pat = 'relpath'
202 202 return _matcher(canonroot, cwd, names, inc, exc, head, dflt_pat, src)
203 203
204 204 def _matcher(canonroot, cwd, names, inc, exc, head, dflt_pat, src):
205 205 """build a function to match a set of file patterns
206 206
207 207 arguments:
208 208 canonroot - the canonical root of the tree you're matching against
209 209 cwd - the current working directory, if relevant
210 210 names - patterns to find
211 211 inc - patterns to include
212 212 exc - patterns to exclude
213 213 head - a regex to prepend to patterns to control whether a match is rooted
214 214
215 215 a pattern is one of:
216 216 'glob:<rooted glob>'
217 217 're:<rooted regexp>'
218 218 'path:<rooted path>'
219 219 'relglob:<relative glob>'
220 220 'relpath:<relative path>'
221 221 'relre:<relative regexp>'
222 222 '<rooted path or regexp>'
223 223
224 224 returns:
225 225 a 3-tuple containing
226 226 - list of explicit non-pattern names passed in
227 227 - a bool match(filename) function
228 228 - a bool indicating if any patterns were passed in
229 229
230 230 todo:
231 231 make head regex a rooted bool
232 232 """
233 233
234 234 def contains_glob(name):
235 235 for c in name:
236 236 if c in _globchars: return True
237 237 return False
238 238
239 239 def regex(kind, name, tail):
240 240 '''convert a pattern into a regular expression'''
241 241 if kind == 're':
242 242 return name
243 243 elif kind == 'path':
244 244 return '^' + re.escape(name) + '(?:/|$)'
245 245 elif kind == 'relglob':
246 246 return head + globre(name, '(?:|.*/)', tail)
247 247 elif kind == 'relpath':
248 248 return head + re.escape(name) + tail
249 249 elif kind == 'relre':
250 250 if name.startswith('^'):
251 251 return name
252 252 return '.*' + name
253 253 return head + globre(name, '', tail)
254 254
255 255 def matchfn(pats, tail):
256 256 """build a matching function from a set of patterns"""
257 257 if not pats:
258 258 return
259 259 matches = []
260 260 for k, p in pats:
261 261 try:
262 262 pat = '(?:%s)' % regex(k, p, tail)
263 263 matches.append(re.compile(pat).match)
264 264 except re.error:
265 265 if src: raise Abort("%s: invalid pattern (%s): %s" % (src, k, p))
266 266 else: raise Abort("invalid pattern (%s): %s" % (k, p))
267 267
268 268 def buildfn(text):
269 269 for m in matches:
270 270 r = m(text)
271 271 if r:
272 272 return r
273 273
274 274 return buildfn
275 275
276 276 def globprefix(pat):
277 277 '''return the non-glob prefix of a path, e.g. foo/* -> foo'''
278 278 root = []
279 279 for p in pat.split(os.sep):
280 280 if contains_glob(p): break
281 281 root.append(p)
282 282 return '/'.join(root)
283 283
284 284 pats = []
285 285 files = []
286 286 roots = []
287 287 for kind, name in [patkind(p, dflt_pat) for p in names]:
288 288 if kind in ('glob', 'relpath'):
289 289 name = canonpath(canonroot, cwd, name)
290 290 if name == '':
291 291 kind, name = 'glob', '**'
292 292 if kind in ('glob', 'path', 're'):
293 293 pats.append((kind, name))
294 294 if kind == 'glob':
295 295 root = globprefix(name)
296 296 if root: roots.append(root)
297 297 elif kind == 'relpath':
298 298 files.append((kind, name))
299 299 roots.append(name)
300 300
301 301 patmatch = matchfn(pats, '$') or always
302 302 filematch = matchfn(files, '(?:/|$)') or always
303 303 incmatch = always
304 304 if inc:
305 305 incmatch = matchfn(map(patkind, inc), '(?:/|$)')
306 306 excmatch = lambda fn: False
307 307 if exc:
308 308 excmatch = matchfn(map(patkind, exc), '(?:/|$)')
309 309
310 310 return (roots,
311 311 lambda fn: (incmatch(fn) and not excmatch(fn) and
312 312 (fn.endswith('/') or
313 313 (not pats and not files) or
314 314 (pats and patmatch(fn)) or
315 315 (files and filematch(fn)))),
316 316 (inc or exc or (pats and pats != [('glob', '**')])) and True)
317 317
318 318 def system(cmd, errprefix=None):
319 319 """execute a shell command that must succeed"""
320 320 rc = os.system(cmd)
321 321 if rc:
322 322 errmsg = "%s %s" % (os.path.basename(cmd.split(None, 1)[0]),
323 323 explain_exit(rc)[0])
324 324 if errprefix:
325 325 errmsg = "%s: %s" % (errprefix, errmsg)
326 326 raise Abort(errmsg)
327 327
328 328 def rename(src, dst):
329 329 """forcibly rename a file"""
330 330 try:
331 331 os.rename(src, dst)
332 332 except:
333 333 os.unlink(dst)
334 334 os.rename(src, dst)
335 335
336 336 def unlink(f):
337 337 """unlink and remove the directory if it is empty"""
338 338 os.unlink(f)
339 339 # try removing directories that might now be empty
340 340 try: os.removedirs(os.path.dirname(f))
341 341 except: pass
342 342
343 343 def copyfiles(src, dst, hardlink=None):
344 344 """Copy a directory tree using hardlinks if possible"""
345 345
346 346 if hardlink is None:
347 347 hardlink = (os.stat(src).st_dev ==
348 348 os.stat(os.path.dirname(dst)).st_dev)
349 349
350 350 if os.path.isdir(src):
351 351 os.mkdir(dst)
352 352 for name in os.listdir(src):
353 353 srcname = os.path.join(src, name)
354 354 dstname = os.path.join(dst, name)
355 355 copyfiles(srcname, dstname, hardlink)
356 356 else:
357 357 if hardlink:
358 358 try:
359 359 os_link(src, dst)
360 360 except:
361 361 hardlink = False
362 362 shutil.copy(src, dst)
363 363 else:
364 364 shutil.copy(src, dst)
365 365
366 366 def opener(base):
367 367 """
368 368 return a function that opens files relative to base
369 369
370 370 this function is used to hide the details of COW semantics and
371 371 remote file access from higher level code.
372 372 """
373 373 p = base
374 374
375 375 def mktempcopy(name):
376 376 d, fn = os.path.split(name)
377 377 fd, temp = tempfile.mkstemp(prefix=fn, dir=d)
378 378 fp = os.fdopen(fd, "wb")
379 379 try:
380 380 fp.write(file(name, "rb").read())
381 381 except:
382 382 try: os.unlink(temp)
383 383 except: pass
384 384 raise
385 385 fp.close()
386 386 st = os.lstat(name)
387 387 os.chmod(temp, st.st_mode)
388 388 return temp
389 389
390 390 class atomicfile(file):
391 391 """the file will only be copied on close"""
392 392 def __init__(self, name, mode, atomic=False):
393 393 self.__name = name
394 394 self.temp = mktempcopy(name)
395 395 file.__init__(self, self.temp, mode)
396 396 def close(self):
397 397 if not self.closed:
398 398 file.close(self)
399 399 rename(self.temp, self.__name)
400 400 def __del__(self):
401 401 self.close()
402 402
403 403 def o(path, mode="r", text=False, atomic=False):
404 404 f = os.path.join(p, path)
405 405
406 406 if not text:
407 407 mode += "b" # for that other OS
408 408
409 409 if mode[0] != "r":
410 410 try:
411 411 nlink = nlinks(f)
412 412 except OSError:
413 413 d = os.path.dirname(f)
414 414 if not os.path.isdir(d):
415 415 os.makedirs(d)
416 416 else:
417 417 if atomic:
418 418 return atomicfile(f, mode)
419 419 if nlink > 1:
420 420 rename(mktempcopy(f), f)
421 421 return file(f, mode)
422 422
423 423 return o
424 424
425 425 def _makelock_file(info, pathname):
426 426 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
427 427 os.write(ld, info)
428 428 os.close(ld)
429 429
430 430 def _readlock_file(pathname):
431 431 return file(pathname).read()
432 432
433 433 def nlinks(pathname):
434 434 """Return number of hardlinks for the given file."""
435 435 return os.stat(pathname).st_nlink
436 436
437 437 if hasattr(os, 'link'):
438 438 os_link = os.link
439 439 else:
440 440 def os_link(src, dst):
441 441 raise OSError(0, _("Hardlinks not supported"))
442 442
443 443 # Platform specific variants
444 444 if os.name == 'nt':
445 445 demandload(globals(), "msvcrt")
446 446 nulldev = 'NUL:'
447 447
448 448 class winstdout:
449 449 '''stdout on windows misbehaves if sent through a pipe'''
450 450
451 451 def __init__(self, fp):
452 452 self.fp = fp
453 453
454 454 def __getattr__(self, key):
455 455 return getattr(self.fp, key)
456 456
457 457 def close(self):
458 458 try:
459 459 self.fp.close()
460 460 except: pass
461 461
462 462 def write(self, s):
463 463 try:
464 464 return self.fp.write(s)
465 465 except IOError, inst:
466 466 if inst.errno != 0: raise
467 467 self.close()
468 468 raise IOError(errno.EPIPE, 'Broken pipe')
469 469
470 470 sys.stdout = winstdout(sys.stdout)
471 471
472 472 try:
473 473 import win32api, win32process
474 474 filename = win32process.GetModuleFileNameEx(win32api.GetCurrentProcess(), 0)
475 475 systemrc = os.path.join(os.path.dirname(filename), 'mercurial.ini')
476 476
477 477 except ImportError:
478 478 systemrc = r'c:\mercurial\mercurial.ini'
479 479 pass
480 480
481 481 rcpath = (systemrc,
482 482 os.path.join(os.path.expanduser('~'), 'mercurial.ini'))
483 483
484 484 def parse_patch_output(output_line):
485 485 """parses the output produced by patch and returns the file name"""
486 486 pf = output_line[14:]
487 487 if pf[0] == '`':
488 488 pf = pf[1:-1] # Remove the quotes
489 489 return pf
490 490
491 491 try: # ActivePython can create hard links using win32file module
492 492 import win32file
493 493
494 494 def os_link(src, dst): # NB will only succeed on NTFS
495 495 win32file.CreateHardLink(dst, src)
496 496
497 497 def nlinks(pathname):
498 498 """Return number of hardlinks for the given file."""
499 499 try:
500 500 fh = win32file.CreateFile(pathname,
501 501 win32file.GENERIC_READ, win32file.FILE_SHARE_READ,
502 502 None, win32file.OPEN_EXISTING, 0, None)
503 503 res = win32file.GetFileInformationByHandle(fh)
504 504 fh.Close()
505 505 return res[7]
506 506 except:
507 507 return os.stat(pathname).st_nlink
508 508
509 509 except ImportError:
510 510 pass
511 511
512 512 def is_exec(f, last):
513 513 return last
514 514
515 515 def set_exec(f, mode):
516 516 pass
517 517
518 518 def set_binary(fd):
519 519 msvcrt.setmode(fd.fileno(), os.O_BINARY)
520 520
521 521 def pconvert(path):
522 522 return path.replace("\\", "/")
523 523
524 524 def localpath(path):
525 525 return path.replace('/', '\\')
526 526
527 527 def normpath(path):
528 528 return pconvert(os.path.normpath(path))
529 529
530 530 makelock = _makelock_file
531 531 readlock = _readlock_file
532 532
533 533 def explain_exit(code):
534 534 return _("exited with status %d") % code, code
535 535
536 536 else:
537 537 nulldev = '/dev/null'
538 538
539 539 def rcfiles(path):
540 540 rcs = [os.path.join(path, 'hgrc')]
541 541 rcdir = os.path.join(path, 'hgrc.d')
542 542 try:
543 543 rcs.extend([os.path.join(rcdir, f) for f in os.listdir(rcdir)
544 544 if f.endswith(".rc")])
545 545 except OSError, inst: pass
546 546 return rcs
547 547 rcpath = []
548 548 if len(sys.argv) > 0:
549 549 rcpath.extend(rcfiles(os.path.dirname(sys.argv[0]) + '/../etc/mercurial'))
550 550 rcpath.extend(rcfiles('/etc/mercurial'))
551 551 rcpath.append(os.path.expanduser('~/.hgrc'))
552 552 rcpath = [os.path.normpath(f) for f in rcpath]
553 553
554 554 def parse_patch_output(output_line):
555 555 """parses the output produced by patch and returns the file name"""
556 556 pf = output_line[14:]
557 557 if pf.startswith("'") and pf.endswith("'") and pf.find(" ") >= 0:
558 558 pf = pf[1:-1] # Remove the quotes
559 559 return pf
560 560
561 561 def is_exec(f, last):
562 562 """check whether a file is executable"""
563 563 return (os.stat(f).st_mode & 0100 != 0)
564 564
565 565 def set_exec(f, mode):
566 566 s = os.stat(f).st_mode
567 567 if (s & 0100 != 0) == mode:
568 568 return
569 569 if mode:
570 570 # Turn on +x for every +r bit when making a file executable
571 571 # and obey umask.
572 572 umask = os.umask(0)
573 573 os.umask(umask)
574 574 os.chmod(f, s | (s & 0444) >> 2 & ~umask)
575 575 else:
576 576 os.chmod(f, s & 0666)
577 577
578 578 def set_binary(fd):
579 579 pass
580 580
581 581 def pconvert(path):
582 582 return path
583 583
584 584 def localpath(path):
585 585 return path
586 586
587 587 normpath = os.path.normpath
588 588
589 589 def makelock(info, pathname):
590 590 try:
591 591 os.symlink(info, pathname)
592 592 except OSError, why:
593 593 if why.errno == errno.EEXIST:
594 594 raise
595 595 else:
596 596 _makelock_file(info, pathname)
597 597
598 598 def readlock(pathname):
599 599 try:
600 600 return os.readlink(pathname)
601 601 except OSError, why:
602 602 if why.errno == errno.EINVAL:
603 603 return _readlock_file(pathname)
604 604 else:
605 605 raise
606 606
607 607 def explain_exit(code):
608 608 """return a 2-tuple (desc, code) describing a process's status"""
609 609 if os.WIFEXITED(code):
610 610 val = os.WEXITSTATUS(code)
611 611 return _("exited with status %d") % val, val
612 612 elif os.WIFSIGNALED(code):
613 613 val = os.WTERMSIG(code)
614 614 return _("killed by signal %d") % val, val
615 615 elif os.WIFSTOPPED(code):
616 616 val = os.WSTOPSIG(code)
617 617 return _("stopped by signal %d") % val, val
618 618 raise ValueError(_("invalid exit code"))
619 619
620 620 class chunkbuffer(object):
621 621 """Allow arbitrary sized chunks of data to be efficiently read from an
622 622 iterator over chunks of arbitrary size."""
623 623
624 624 def __init__(self, in_iter, targetsize = 2**16):
625 625 """in_iter is the iterator that's iterating over the input chunks.
626 626 targetsize is how big a buffer to try to maintain."""
627 627 self.in_iter = iter(in_iter)
628 628 self.buf = ''
629 629 self.targetsize = int(targetsize)
630 630 if self.targetsize <= 0:
631 631 raise ValueError(_("targetsize must be greater than 0, was %d") %
632 632 targetsize)
633 633 self.iterempty = False
634 634
635 635 def fillbuf(self):
636 636 """Ignore target size; read every chunk from iterator until empty."""
637 637 if not self.iterempty:
638 638 collector = cStringIO.StringIO()
639 639 collector.write(self.buf)
640 640 for ch in self.in_iter:
641 641 collector.write(ch)
642 642 self.buf = collector.getvalue()
643 643 self.iterempty = True
644 644
645 645 def read(self, l):
646 646 """Read L bytes of data from the iterator of chunks of data.
647 647 Returns less than L bytes if the iterator runs dry."""
648 648 if l > len(self.buf) and not self.iterempty:
649 649 # Clamp to a multiple of self.targetsize
650 650 targetsize = self.targetsize * ((l // self.targetsize) + 1)
651 651 collector = cStringIO.StringIO()
652 652 collector.write(self.buf)
653 653 collected = len(self.buf)
654 654 for chunk in self.in_iter:
655 655 collector.write(chunk)
656 656 collected += len(chunk)
657 657 if collected >= targetsize:
658 658 break
659 659 if collected < targetsize:
660 660 self.iterempty = True
661 661 self.buf = collector.getvalue()
662 662 s, self.buf = self.buf[:l], buffer(self.buf, l)
663 663 return s
664 664
665 665 def filechunkiter(f, size = 65536):
666 666 """Create a generator that produces all the data in the file size
667 667 (default 65536) bytes at a time. Chunks may be less than size
668 668 bytes if the chunk is the last chunk in the file, or the file is a
669 669 socket or some other type of file that sometimes reads less data
670 670 than is requested."""
671 671 s = f.read(size)
672 672 while len(s) > 0:
673 673 yield s
674 674 s = f.read(size)
675 675
676 676 def makedate():
677 677 lt = time.localtime()
678 678 if lt[8] == 1 and time.daylight:
679 679 tz = time.altzone
680 680 else:
681 681 tz = time.timezone
682 682 return time.mktime(lt), tz
683 683
684 684 def datestr(date=None, format='%c'):
685 685 """represent a (unixtime, offset) tuple as a localized time.
686 686 unixtime is seconds since the epoch, and offset is the time zone's
687 687 number of seconds away from UTC."""
688 688 t, tz = date or makedate()
689 689 return ("%s %+03d%02d" %
690 690 (time.strftime(format, time.gmtime(float(t) - tz)),
691 691 -tz / 3600,
692 692 ((-tz % 3600) / 60)))
693
694 def walkrepos(path):
695 '''yield every hg repository under path, recursively.'''
696 def errhandler(err):
697 if err.filename == path:
698 raise err
699
700 for root, dirs, files in os.walk(path, onerror=errhandler):
701 for d in dirs:
702 if d == '.hg':
703 yield root
704 dirs[:] = []
705 break
General Comments 0
You need to be logged in to leave comments. Login now