##// END OF EJS Templates
fix warnings spotted by pychecker
Benoit Boissinot -
r3131:cff3c58a default
parent child Browse files
Show More
@@ -1,981 +1,980 b''
1 1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 import os
10 10 import os.path
11 11 import mimetypes
12 12 from mercurial.demandload import demandload
13 13 demandload(globals(), "re zlib ConfigParser mimetools cStringIO sys tempfile")
14 14 demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,streamclone,patch")
15 15 demandload(globals(), "mercurial:templater")
16 16 demandload(globals(), "mercurial.hgweb.common:get_mtime,staticfile")
17 17 from mercurial.node import *
18 18 from mercurial.i18n import gettext as _
19 19
20 20 def _up(p):
21 21 if p[0] != "/":
22 22 p = "/" + p
23 23 if p[-1] == "/":
24 24 p = p[:-1]
25 25 up = os.path.dirname(p)
26 26 if up == "/":
27 27 return "/"
28 28 return up + "/"
29 29
30 30 class hgweb(object):
31 31 def __init__(self, repo, name=None):
32 32 if type(repo) == type(""):
33 33 self.repo = hg.repository(ui.ui(), repo)
34 34 else:
35 35 self.repo = repo
36 36
37 37 self.mtime = -1
38 38 self.reponame = name
39 39 self.archives = 'zip', 'gz', 'bz2'
40 40 self.stripecount = 1
41 41 self.templatepath = self.repo.ui.config("web", "templates",
42 42 templater.templatepath())
43 43
44 44 def refresh(self):
45 45 mtime = get_mtime(self.repo.root)
46 46 if mtime != self.mtime:
47 47 self.mtime = mtime
48 48 self.repo = hg.repository(self.repo.ui, self.repo.root)
49 49 self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10))
50 50 self.stripecount = int(self.repo.ui.config("web", "stripes", 1))
51 51 self.maxshortchanges = int(self.repo.ui.config("web", "maxshortchanges", 60))
52 52 self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10))
53 53 self.allowpull = self.repo.ui.configbool("web", "allowpull", True)
54 54
55 55 def archivelist(self, nodeid):
56 56 allowed = self.repo.ui.configlist("web", "allow_archive")
57 57 for i in self.archives:
58 58 if i in allowed or self.repo.ui.configbool("web", "allow" + i):
59 59 yield {"type" : i, "node" : nodeid, "url": ""}
60 60
61 61 def listfiles(self, files, mf):
62 62 for f in files[:self.maxfiles]:
63 63 yield self.t("filenodelink", node=hex(mf[f]), file=f)
64 64 if len(files) > self.maxfiles:
65 65 yield self.t("fileellipses")
66 66
67 67 def listfilediffs(self, files, changeset):
68 68 for f in files[:self.maxfiles]:
69 69 yield self.t("filedifflink", node=hex(changeset), file=f)
70 70 if len(files) > self.maxfiles:
71 71 yield self.t("fileellipses")
72 72
73 73 def siblings(self, siblings=[], rev=None, hiderev=None, **args):
74 74 if not rev:
75 75 rev = lambda x: ""
76 76 siblings = [s for s in siblings if s != nullid]
77 77 if len(siblings) == 1 and rev(siblings[0]) == hiderev:
78 78 return
79 79 for s in siblings:
80 80 yield dict(node=hex(s), rev=rev(s), **args)
81 81
82 82 def renamelink(self, fl, node):
83 83 r = fl.renamed(node)
84 84 if r:
85 85 return [dict(file=r[0], node=hex(r[1]))]
86 86 return []
87 87
88 88 def showtag(self, t1, node=nullid, **args):
89 89 for t in self.repo.nodetags(node):
90 90 yield self.t(t1, tag=t, **args)
91 91
92 92 def diff(self, node1, node2, files):
93 93 def filterfiles(filters, files):
94 94 l = [x for x in files if x in filters]
95 95
96 96 for t in filters:
97 97 if t and t[-1] != os.sep:
98 98 t += os.sep
99 99 l += [x for x in files if x.startswith(t)]
100 100 return l
101 101
102 102 parity = [0]
103 103 def diffblock(diff, f, fn):
104 104 yield self.t("diffblock",
105 105 lines=prettyprintlines(diff),
106 106 parity=parity[0],
107 107 file=f,
108 108 filenode=hex(fn or nullid))
109 109 parity[0] = 1 - parity[0]
110 110
111 111 def prettyprintlines(diff):
112 112 for l in diff.splitlines(1):
113 113 if l.startswith('+'):
114 114 yield self.t("difflineplus", line=l)
115 115 elif l.startswith('-'):
116 116 yield self.t("difflineminus", line=l)
117 117 elif l.startswith('@'):
118 118 yield self.t("difflineat", line=l)
119 119 else:
120 120 yield self.t("diffline", line=l)
121 121
122 122 r = self.repo
123 123 cl = r.changelog
124 124 mf = r.manifest
125 125 change1 = cl.read(node1)
126 126 change2 = cl.read(node2)
127 127 mmap1 = mf.read(change1[0])
128 128 mmap2 = mf.read(change2[0])
129 129 date1 = util.datestr(change1[2])
130 130 date2 = util.datestr(change2[2])
131 131
132 132 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
133 133 if files:
134 134 modified, added, removed = map(lambda x: filterfiles(files, x),
135 135 (modified, added, removed))
136 136
137 137 diffopts = patch.diffopts(self.repo.ui)
138 138 for f in modified:
139 139 to = r.file(f).read(mmap1[f])
140 140 tn = r.file(f).read(mmap2[f])
141 141 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
142 142 opts=diffopts), f, tn)
143 143 for f in added:
144 144 to = None
145 145 tn = r.file(f).read(mmap2[f])
146 146 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
147 147 opts=diffopts), f, tn)
148 148 for f in removed:
149 149 to = r.file(f).read(mmap1[f])
150 150 tn = None
151 151 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
152 152 opts=diffopts), f, tn)
153 153
154 154 def changelog(self, pos, shortlog=False):
155 155 def changenav(**map):
156 156 def seq(factor, maxchanges=None):
157 157 if maxchanges:
158 158 yield maxchanges
159 159 if maxchanges >= 20 and maxchanges <= 40:
160 160 yield 50
161 161 else:
162 162 yield 1 * factor
163 163 yield 3 * factor
164 164 for f in seq(factor * 10):
165 165 yield f
166 166
167 167 l = []
168 168 last = 0
169 169 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
170 170 for f in seq(1, maxchanges):
171 171 if f < maxchanges or f <= last:
172 172 continue
173 173 if f > count:
174 174 break
175 175 last = f
176 176 r = "%d" % f
177 177 if pos + f < count:
178 178 l.append(("+" + r, pos + f))
179 179 if pos - f >= 0:
180 180 l.insert(0, ("-" + r, pos - f))
181 181
182 182 yield {"rev": 0, "label": "(0)"}
183 183
184 184 for label, rev in l:
185 185 yield {"label": label, "rev": rev}
186 186
187 187 yield {"label": "tip", "rev": "tip"}
188 188
189 189 def changelist(**map):
190 190 parity = (start - end) & 1
191 191 cl = self.repo.changelog
192 192 l = [] # build a list in forward order for efficiency
193 193 for i in range(start, end):
194 194 n = cl.node(i)
195 195 changes = cl.read(n)
196 196 hn = hex(n)
197 197
198 198 l.insert(0, {"parity": parity,
199 199 "author": changes[1],
200 200 "parent": self.siblings(cl.parents(n), cl.rev,
201 201 cl.rev(n) - 1),
202 202 "child": self.siblings(cl.children(n), cl.rev,
203 203 cl.rev(n) + 1),
204 204 "changelogtag": self.showtag("changelogtag",n),
205 205 "manifest": hex(changes[0]),
206 206 "desc": changes[4],
207 207 "date": changes[2],
208 208 "files": self.listfilediffs(changes[3], n),
209 209 "rev": i,
210 210 "node": hn})
211 211 parity = 1 - parity
212 212
213 213 for e in l:
214 214 yield e
215 215
216 216 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
217 217 cl = self.repo.changelog
218 218 mf = cl.read(cl.tip())[0]
219 219 count = cl.count()
220 220 start = max(0, pos - maxchanges + 1)
221 221 end = min(count, start + maxchanges)
222 222 pos = end - 1
223 223
224 224 yield self.t(shortlog and 'shortlog' or 'changelog',
225 225 changenav=changenav,
226 226 manifest=hex(mf),
227 227 rev=pos, changesets=count, entries=changelist,
228 228 archives=self.archivelist("tip"))
229 229
230 230 def search(self, query):
231 231
232 232 def changelist(**map):
233 233 cl = self.repo.changelog
234 234 count = 0
235 235 qw = query.lower().split()
236 236
237 237 def revgen():
238 238 for i in range(cl.count() - 1, 0, -100):
239 239 l = []
240 240 for j in range(max(0, i - 100), i):
241 241 n = cl.node(j)
242 242 changes = cl.read(n)
243 243 l.append((n, j, changes))
244 244 l.reverse()
245 245 for e in l:
246 246 yield e
247 247
248 248 for n, i, changes in revgen():
249 249 miss = 0
250 250 for q in qw:
251 251 if not (q in changes[1].lower() or
252 252 q in changes[4].lower() or
253 253 q in " ".join(changes[3][:20]).lower()):
254 254 miss = 1
255 255 break
256 256 if miss:
257 257 continue
258 258
259 259 count += 1
260 260 hn = hex(n)
261 261
262 262 yield self.t('searchentry',
263 263 parity=self.stripes(count),
264 264 author=changes[1],
265 265 parent=self.siblings(cl.parents(n), cl.rev),
266 266 child=self.siblings(cl.children(n), cl.rev),
267 267 changelogtag=self.showtag("changelogtag",n),
268 268 manifest=hex(changes[0]),
269 269 desc=changes[4],
270 270 date=changes[2],
271 271 files=self.listfilediffs(changes[3], n),
272 272 rev=i,
273 273 node=hn)
274 274
275 275 if count >= self.maxchanges:
276 276 break
277 277
278 278 cl = self.repo.changelog
279 279 mf = cl.read(cl.tip())[0]
280 280
281 281 yield self.t('search',
282 282 query=query,
283 283 manifest=hex(mf),
284 284 entries=changelist)
285 285
286 286 def changeset(self, nodeid):
287 287 cl = self.repo.changelog
288 288 n = self.repo.lookup(nodeid)
289 289 nodeid = hex(n)
290 290 changes = cl.read(n)
291 291 p1 = cl.parents(n)[0]
292 292
293 293 files = []
294 294 mf = self.repo.manifest.read(changes[0])
295 295 for f in changes[3]:
296 296 files.append(self.t("filenodelink",
297 297 filenode=hex(mf.get(f, nullid)), file=f))
298 298
299 299 def diff(**map):
300 300 yield self.diff(p1, n, None)
301 301
302 302 yield self.t('changeset',
303 303 diff=diff,
304 304 rev=cl.rev(n),
305 305 node=nodeid,
306 306 parent=self.siblings(cl.parents(n), cl.rev),
307 307 child=self.siblings(cl.children(n), cl.rev),
308 308 changesettag=self.showtag("changesettag",n),
309 309 manifest=hex(changes[0]),
310 310 author=changes[1],
311 311 desc=changes[4],
312 312 date=changes[2],
313 313 files=files,
314 314 archives=self.archivelist(nodeid))
315 315
316 316 def filelog(self, f, filenode):
317 317 cl = self.repo.changelog
318 318 fl = self.repo.file(f)
319 319 filenode = hex(fl.lookup(filenode))
320 320 count = fl.count()
321 321
322 322 def entries(**map):
323 323 l = []
324 324 parity = (count - 1) & 1
325 325
326 326 for i in range(count):
327 327 n = fl.node(i)
328 328 lr = fl.linkrev(n)
329 329 cn = cl.node(lr)
330 330 cs = cl.read(cl.node(lr))
331 331
332 332 l.insert(0, {"parity": parity,
333 333 "filenode": hex(n),
334 334 "filerev": i,
335 335 "file": f,
336 336 "node": hex(cn),
337 337 "author": cs[1],
338 338 "date": cs[2],
339 339 "rename": self.renamelink(fl, n),
340 340 "parent": self.siblings(fl.parents(n),
341 341 fl.rev, file=f),
342 342 "child": self.siblings(fl.children(n),
343 343 fl.rev, file=f),
344 344 "desc": cs[4]})
345 345 parity = 1 - parity
346 346
347 347 for e in l:
348 348 yield e
349 349
350 350 yield self.t("filelog", file=f, filenode=filenode, entries=entries)
351 351
352 352 def filerevision(self, f, node):
353 353 fl = self.repo.file(f)
354 354 n = fl.lookup(node)
355 355 node = hex(n)
356 356 text = fl.read(n)
357 357 changerev = fl.linkrev(n)
358 358 cl = self.repo.changelog
359 359 cn = cl.node(changerev)
360 360 cs = cl.read(cn)
361 361 mfn = cs[0]
362 362
363 363 mt = mimetypes.guess_type(f)[0]
364 364 rawtext = text
365 365 if util.binary(text):
366 366 mt = mt or 'application/octet-stream'
367 367 text = "(binary:%s)" % mt
368 368 mt = mt or 'text/plain'
369 369
370 370 def lines():
371 371 for l, t in enumerate(text.splitlines(1)):
372 372 yield {"line": t,
373 373 "linenumber": "% 6d" % (l + 1),
374 374 "parity": self.stripes(l)}
375 375
376 376 yield self.t("filerevision",
377 377 file=f,
378 378 filenode=node,
379 379 path=_up(f),
380 380 text=lines(),
381 381 raw=rawtext,
382 382 mimetype=mt,
383 383 rev=changerev,
384 384 node=hex(cn),
385 385 manifest=hex(mfn),
386 386 author=cs[1],
387 387 date=cs[2],
388 388 parent=self.siblings(fl.parents(n), fl.rev, file=f),
389 389 child=self.siblings(fl.children(n), fl.rev, file=f),
390 390 rename=self.renamelink(fl, n),
391 391 permissions=self.repo.manifest.read(mfn).execf(f))
392 392
393 393 def fileannotate(self, f, node):
394 394 bcache = {}
395 395 ncache = {}
396 396 fl = self.repo.file(f)
397 397 n = fl.lookup(node)
398 398 node = hex(n)
399 399 changerev = fl.linkrev(n)
400 400
401 401 cl = self.repo.changelog
402 402 cn = cl.node(changerev)
403 403 cs = cl.read(cn)
404 404 mfn = cs[0]
405 405
406 406 def annotate(**map):
407 407 parity = 0
408 408 last = None
409 409 for r, l in fl.annotate(n):
410 410 try:
411 411 cnode = ncache[r]
412 412 except KeyError:
413 413 cnode = ncache[r] = self.repo.changelog.node(r)
414 414
415 415 try:
416 416 name = bcache[r]
417 417 except KeyError:
418 418 cl = self.repo.changelog.read(cnode)
419 419 bcache[r] = name = self.repo.ui.shortuser(cl[1])
420 420
421 421 if last != cnode:
422 422 parity = 1 - parity
423 423 last = cnode
424 424
425 425 yield {"parity": parity,
426 426 "node": hex(cnode),
427 427 "rev": r,
428 428 "author": name,
429 429 "file": f,
430 430 "line": l}
431 431
432 432 yield self.t("fileannotate",
433 433 file=f,
434 434 filenode=node,
435 435 annotate=annotate,
436 436 path=_up(f),
437 437 rev=changerev,
438 438 node=hex(cn),
439 439 manifest=hex(mfn),
440 440 author=cs[1],
441 441 date=cs[2],
442 442 rename=self.renamelink(fl, n),
443 443 parent=self.siblings(fl.parents(n), fl.rev, file=f),
444 444 child=self.siblings(fl.children(n), fl.rev, file=f),
445 445 permissions=self.repo.manifest.read(mfn).execf(f))
446 446
447 447 def manifest(self, mnode, path):
448 448 man = self.repo.manifest
449 449 mn = man.lookup(mnode)
450 450 mnode = hex(mn)
451 451 mf = man.read(mn)
452 452 rev = man.rev(mn)
453 453 changerev = man.linkrev(mn)
454 454 node = self.repo.changelog.node(changerev)
455 455
456 456 files = {}
457 457
458 458 p = path[1:]
459 459 if p and p[-1] != "/":
460 460 p += "/"
461 461 l = len(p)
462 462
463 463 for f,n in mf.items():
464 464 if f[:l] != p:
465 465 continue
466 466 remain = f[l:]
467 467 if "/" in remain:
468 468 short = remain[:remain.index("/") + 1] # bleah
469 469 files[short] = (f, None)
470 470 else:
471 471 short = os.path.basename(remain)
472 472 files[short] = (f, n)
473 473
474 474 def filelist(**map):
475 475 parity = 0
476 476 fl = files.keys()
477 477 fl.sort()
478 478 for f in fl:
479 479 full, fnode = files[f]
480 480 if not fnode:
481 481 continue
482 482
483 483 yield {"file": full,
484 484 "manifest": mnode,
485 485 "filenode": hex(fnode),
486 486 "parity": self.stripes(parity),
487 487 "basename": f,
488 488 "permissions": mf.execf(full)}
489 489 parity += 1
490 490
491 491 def dirlist(**map):
492 492 parity = 0
493 493 fl = files.keys()
494 494 fl.sort()
495 495 for f in fl:
496 496 full, fnode = files[f]
497 497 if fnode:
498 498 continue
499 499
500 500 yield {"parity": self.stripes(parity),
501 501 "path": os.path.join(path, f),
502 502 "manifest": mnode,
503 503 "basename": f[:-1]}
504 504 parity += 1
505 505
506 506 yield self.t("manifest",
507 507 manifest=mnode,
508 508 rev=rev,
509 509 node=hex(node),
510 510 path=path,
511 511 up=_up(path),
512 512 fentries=filelist,
513 513 dentries=dirlist,
514 514 archives=self.archivelist(hex(node)))
515 515
516 516 def tags(self):
517 517 cl = self.repo.changelog
518 518 mf = cl.read(cl.tip())[0]
519 519
520 520 i = self.repo.tagslist()
521 521 i.reverse()
522 522
523 523 def entries(notip=False, **map):
524 524 parity = 0
525 525 for k,n in i:
526 526 if notip and k == "tip": continue
527 527 yield {"parity": self.stripes(parity),
528 528 "tag": k,
529 529 "tagmanifest": hex(cl.read(n)[0]),
530 530 "date": cl.read(n)[2],
531 531 "node": hex(n)}
532 532 parity += 1
533 533
534 534 yield self.t("tags",
535 535 manifest=hex(mf),
536 536 entries=lambda **x: entries(False, **x),
537 537 entriesnotip=lambda **x: entries(True, **x))
538 538
539 539 def summary(self):
540 540 cl = self.repo.changelog
541 541 mf = cl.read(cl.tip())[0]
542 542
543 543 i = self.repo.tagslist()
544 544 i.reverse()
545 545
546 546 def tagentries(**map):
547 547 parity = 0
548 548 count = 0
549 549 for k,n in i:
550 550 if k == "tip": # skip tip
551 551 continue;
552 552
553 553 count += 1
554 554 if count > 10: # limit to 10 tags
555 555 break;
556 556
557 557 c = cl.read(n)
558 558 m = c[0]
559 559 t = c[2]
560 560
561 561 yield self.t("tagentry",
562 562 parity = self.stripes(parity),
563 563 tag = k,
564 564 node = hex(n),
565 565 date = t,
566 566 tagmanifest = hex(m))
567 567 parity += 1
568 568
569 569 def changelist(**map):
570 570 parity = 0
571 571 cl = self.repo.changelog
572 572 l = [] # build a list in forward order for efficiency
573 573 for i in range(start, end):
574 574 n = cl.node(i)
575 575 changes = cl.read(n)
576 576 hn = hex(n)
577 577 t = changes[2]
578 578
579 579 l.insert(0, self.t(
580 580 'shortlogentry',
581 581 parity = parity,
582 582 author = changes[1],
583 583 manifest = hex(changes[0]),
584 584 desc = changes[4],
585 585 date = t,
586 586 rev = i,
587 587 node = hn))
588 588 parity = 1 - parity
589 589
590 590 yield l
591 591
592 592 cl = self.repo.changelog
593 593 mf = cl.read(cl.tip())[0]
594 594 count = cl.count()
595 595 start = max(0, count - self.maxchanges)
596 596 end = min(count, start + self.maxchanges)
597 597
598 598 yield self.t("summary",
599 599 desc = self.repo.ui.config("web", "description", "unknown"),
600 600 owner = (self.repo.ui.config("ui", "username") or # preferred
601 601 self.repo.ui.config("web", "contact") or # deprecated
602 602 self.repo.ui.config("web", "author", "unknown")), # also
603 603 lastchange = (0, 0), # FIXME
604 604 manifest = hex(mf),
605 605 tags = tagentries,
606 606 shortlog = changelist,
607 607 archives=self.archivelist("tip"))
608 608
609 609 def filediff(self, file, changeset):
610 610 cl = self.repo.changelog
611 611 n = self.repo.lookup(changeset)
612 612 changeset = hex(n)
613 613 p1 = cl.parents(n)[0]
614 614 cs = cl.read(n)
615 615 mf = self.repo.manifest.read(cs[0])
616 616
617 617 def diff(**map):
618 618 yield self.diff(p1, n, [file])
619 619
620 620 yield self.t("filediff",
621 621 file=file,
622 622 filenode=hex(mf.get(file, nullid)),
623 623 node=changeset,
624 624 rev=self.repo.changelog.rev(n),
625 625 parent=self.siblings(cl.parents(n), cl.rev),
626 626 child=self.siblings(cl.children(n), cl.rev),
627 627 diff=diff)
628 628
629 629 archive_specs = {
630 630 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
631 631 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
632 632 'zip': ('application/zip', 'zip', '.zip', None),
633 633 }
634 634
635 635 def archive(self, req, cnode, type_):
636 636 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
637 637 name = "%s-%s" % (reponame, short(cnode))
638 638 mimetype, artype, extension, encoding = self.archive_specs[type_]
639 639 headers = [('Content-type', mimetype),
640 640 ('Content-disposition', 'attachment; filename=%s%s' %
641 641 (name, extension))]
642 642 if encoding:
643 643 headers.append(('Content-encoding', encoding))
644 644 req.header(headers)
645 645 archival.archive(self.repo, req.out, cnode, artype, prefix=name)
646 646
647 647 # add tags to things
648 648 # tags -> list of changesets corresponding to tags
649 649 # find tag, changeset, file
650 650
651 651 def cleanpath(self, path):
652 652 p = util.normpath(path)
653 653 if p[:2] == "..":
654 654 raise Exception("suspicious path")
655 655 return p
656 656
657 657 def run(self):
658 658 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
659 659 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
660 660 import mercurial.hgweb.wsgicgi as wsgicgi
661 661 from request import wsgiapplication
662 662 def make_web_app():
663 663 return self
664 664 wsgicgi.launch(wsgiapplication(make_web_app))
665 665
666 666 def run_wsgi(self, req):
667 667 def header(**map):
668 668 header_file = cStringIO.StringIO(''.join(self.t("header", **map)))
669 669 msg = mimetools.Message(header_file, 0)
670 670 req.header(msg.items())
671 671 yield header_file.read()
672 672
673 673 def rawfileheader(**map):
674 674 req.header([('Content-type', map['mimetype']),
675 675 ('Content-disposition', 'filename=%s' % map['file']),
676 676 ('Content-length', str(len(map['raw'])))])
677 677 yield ''
678 678
679 679 def footer(**map):
680 680 yield self.t("footer",
681 681 motd=self.repo.ui.config("web", "motd", ""),
682 682 **map)
683 683
684 684 def expand_form(form):
685 685 shortcuts = {
686 686 'cl': [('cmd', ['changelog']), ('rev', None)],
687 687 'sl': [('cmd', ['shortlog']), ('rev', None)],
688 688 'cs': [('cmd', ['changeset']), ('node', None)],
689 689 'f': [('cmd', ['file']), ('filenode', None)],
690 690 'fl': [('cmd', ['filelog']), ('filenode', None)],
691 691 'fd': [('cmd', ['filediff']), ('node', None)],
692 692 'fa': [('cmd', ['annotate']), ('filenode', None)],
693 693 'mf': [('cmd', ['manifest']), ('manifest', None)],
694 694 'ca': [('cmd', ['archive']), ('node', None)],
695 695 'tags': [('cmd', ['tags'])],
696 696 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
697 697 'static': [('cmd', ['static']), ('file', None)]
698 698 }
699 699
700 700 for k in shortcuts.iterkeys():
701 701 if form.has_key(k):
702 702 for name, value in shortcuts[k]:
703 703 if value is None:
704 704 value = form[k]
705 705 form[name] = value
706 706 del form[k]
707 707
708 708 self.refresh()
709 709
710 710 expand_form(req.form)
711 711
712 712 m = os.path.join(self.templatepath, "map")
713 713 style = self.repo.ui.config("web", "style", "")
714 714 if req.form.has_key('style'):
715 715 style = req.form['style'][0]
716 716 if style:
717 717 b = os.path.basename("map-" + style)
718 718 p = os.path.join(self.templatepath, b)
719 719 if os.path.isfile(p):
720 720 m = p
721 721
722 722 port = req.env["SERVER_PORT"]
723 723 port = port != "80" and (":" + port) or ""
724 724 uri = req.env["REQUEST_URI"]
725 725 if "?" in uri:
726 726 uri = uri.split("?")[0]
727 727 url = "http://%s%s%s" % (req.env["SERVER_NAME"], port, uri)
728 728 if not self.reponame:
729 729 self.reponame = (self.repo.ui.config("web", "name")
730 730 or uri.strip('/') or self.repo.root)
731 731
732 732 self.t = templater.templater(m, templater.common_filters,
733 733 defaults={"url": url,
734 734 "repo": self.reponame,
735 735 "header": header,
736 736 "footer": footer,
737 737 "rawfileheader": rawfileheader,
738 738 })
739 739
740 740 if not req.form.has_key('cmd'):
741 741 req.form['cmd'] = [self.t.cache['default'],]
742 742
743 743 cmd = req.form['cmd'][0]
744 744
745 745 method = getattr(self, 'do_' + cmd, None)
746 746 if method:
747 747 method(req)
748 748 else:
749 749 req.write(self.t("error"))
750 750
751 751 def stripes(self, parity):
752 752 "make horizontal stripes for easier reading"
753 753 if self.stripecount:
754 754 return (1 + parity / self.stripecount) & 1
755 755 else:
756 756 return 0
757 757
758 758 def do_changelog(self, req):
759 759 hi = self.repo.changelog.count() - 1
760 760 if req.form.has_key('rev'):
761 761 hi = req.form['rev'][0]
762 762 try:
763 763 hi = self.repo.changelog.rev(self.repo.lookup(hi))
764 764 except hg.RepoError:
765 765 req.write(self.search(hi)) # XXX redirect to 404 page?
766 766 return
767 767
768 768 req.write(self.changelog(hi))
769 769
770 770 def do_shortlog(self, req):
771 771 hi = self.repo.changelog.count() - 1
772 772 if req.form.has_key('rev'):
773 773 hi = req.form['rev'][0]
774 774 try:
775 775 hi = self.repo.changelog.rev(self.repo.lookup(hi))
776 776 except hg.RepoError:
777 777 req.write(self.search(hi)) # XXX redirect to 404 page?
778 778 return
779 779
780 780 req.write(self.changelog(hi, shortlog = True))
781 781
782 782 def do_changeset(self, req):
783 783 req.write(self.changeset(req.form['node'][0]))
784 784
785 785 def do_manifest(self, req):
786 786 req.write(self.manifest(req.form['manifest'][0],
787 787 self.cleanpath(req.form['path'][0])))
788 788
789 789 def do_tags(self, req):
790 790 req.write(self.tags())
791 791
792 792 def do_summary(self, req):
793 793 req.write(self.summary())
794 794
795 795 def do_filediff(self, req):
796 796 req.write(self.filediff(self.cleanpath(req.form['file'][0]),
797 797 req.form['node'][0]))
798 798
799 799 def do_file(self, req):
800 800 req.write(self.filerevision(self.cleanpath(req.form['file'][0]),
801 801 req.form['filenode'][0]))
802 802
803 803 def do_annotate(self, req):
804 804 req.write(self.fileannotate(self.cleanpath(req.form['file'][0]),
805 805 req.form['filenode'][0]))
806 806
807 807 def do_filelog(self, req):
808 808 req.write(self.filelog(self.cleanpath(req.form['file'][0]),
809 809 req.form['filenode'][0]))
810 810
811 811 def do_heads(self, req):
812 812 resp = " ".join(map(hex, self.repo.heads())) + "\n"
813 813 req.httphdr("application/mercurial-0.1", length=len(resp))
814 814 req.write(resp)
815 815
816 816 def do_branches(self, req):
817 817 nodes = []
818 818 if req.form.has_key('nodes'):
819 819 nodes = map(bin, req.form['nodes'][0].split(" "))
820 820 resp = cStringIO.StringIO()
821 821 for b in self.repo.branches(nodes):
822 822 resp.write(" ".join(map(hex, b)) + "\n")
823 823 resp = resp.getvalue()
824 824 req.httphdr("application/mercurial-0.1", length=len(resp))
825 825 req.write(resp)
826 826
827 827 def do_between(self, req):
828 nodes = []
829 828 if req.form.has_key('pairs'):
830 829 pairs = [map(bin, p.split("-"))
831 830 for p in req.form['pairs'][0].split(" ")]
832 831 resp = cStringIO.StringIO()
833 832 for b in self.repo.between(pairs):
834 833 resp.write(" ".join(map(hex, b)) + "\n")
835 834 resp = resp.getvalue()
836 835 req.httphdr("application/mercurial-0.1", length=len(resp))
837 836 req.write(resp)
838 837
839 838 def do_changegroup(self, req):
840 839 req.httphdr("application/mercurial-0.1")
841 840 nodes = []
842 841 if not self.allowpull:
843 842 return
844 843
845 844 if req.form.has_key('roots'):
846 845 nodes = map(bin, req.form['roots'][0].split(" "))
847 846
848 847 z = zlib.compressobj()
849 848 f = self.repo.changegroup(nodes, 'serve')
850 849 while 1:
851 850 chunk = f.read(4096)
852 851 if not chunk:
853 852 break
854 853 req.write(z.compress(chunk))
855 854
856 855 req.write(z.flush())
857 856
858 857 def do_archive(self, req):
859 858 changeset = self.repo.lookup(req.form['node'][0])
860 859 type_ = req.form['type'][0]
861 860 allowed = self.repo.ui.configlist("web", "allow_archive")
862 861 if (type_ in self.archives and (type_ in allowed or
863 862 self.repo.ui.configbool("web", "allow" + type_, False))):
864 863 self.archive(req, changeset, type_)
865 864 return
866 865
867 866 req.write(self.t("error"))
868 867
869 868 def do_static(self, req):
870 869 fname = req.form['file'][0]
871 870 static = self.repo.ui.config("web", "static",
872 871 os.path.join(self.templatepath,
873 872 "static"))
874 873 req.write(staticfile(static, fname, req)
875 874 or self.t("error", error="%r not found" % fname))
876 875
877 876 def do_capabilities(self, req):
878 877 caps = ['unbundle']
879 878 if self.repo.ui.configbool('server', 'uncompressed'):
880 879 caps.append('stream=%d' % self.repo.revlogversion)
881 880 resp = ' '.join(caps)
882 881 req.httphdr("application/mercurial-0.1", length=len(resp))
883 882 req.write(resp)
884 883
885 884 def check_perm(self, req, op, default):
886 885 '''check permission for operation based on user auth.
887 886 return true if op allowed, else false.
888 887 default is policy to use if no config given.'''
889 888
890 889 user = req.env.get('REMOTE_USER')
891 890
892 891 deny = self.repo.ui.configlist('web', 'deny_' + op)
893 892 if deny and (not user or deny == ['*'] or user in deny):
894 893 return False
895 894
896 895 allow = self.repo.ui.configlist('web', 'allow_' + op)
897 896 return (allow and (allow == ['*'] or user in allow)) or default
898 897
899 898 def do_unbundle(self, req):
900 899 def bail(response, headers={}):
901 900 length = int(req.env['CONTENT_LENGTH'])
902 901 for s in util.filechunkiter(req, limit=length):
903 902 # drain incoming bundle, else client will not see
904 903 # response when run outside cgi script
905 904 pass
906 905 req.httphdr("application/mercurial-0.1", headers=headers)
907 906 req.write('0\n')
908 907 req.write(response)
909 908
910 909 # require ssl by default, auth info cannot be sniffed and
911 910 # replayed
912 911 ssl_req = self.repo.ui.configbool('web', 'push_ssl', True)
913 912 if ssl_req:
914 913 if not req.env.get('HTTPS'):
915 914 bail(_('ssl required\n'))
916 915 return
917 916 proto = 'https'
918 917 else:
919 918 proto = 'http'
920 919
921 920 # do not allow push unless explicitly allowed
922 921 if not self.check_perm(req, 'push', False):
923 922 bail(_('push not authorized\n'),
924 923 headers={'status': '401 Unauthorized'})
925 924 return
926 925
927 926 req.httphdr("application/mercurial-0.1")
928 927
929 928 their_heads = req.form['heads'][0].split(' ')
930 929
931 930 def check_heads():
932 931 heads = map(hex, self.repo.heads())
933 932 return their_heads == [hex('force')] or their_heads == heads
934 933
935 934 # fail early if possible
936 935 if not check_heads():
937 936 bail(_('unsynced changes\n'))
938 937 return
939 938
940 939 # do not lock repo until all changegroup data is
941 940 # streamed. save to temporary file.
942 941
943 942 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
944 943 fp = os.fdopen(fd, 'wb+')
945 944 try:
946 945 length = int(req.env['CONTENT_LENGTH'])
947 946 for s in util.filechunkiter(req, limit=length):
948 947 fp.write(s)
949 948
950 949 lock = self.repo.lock()
951 950 try:
952 951 if not check_heads():
953 952 req.write('0\n')
954 953 req.write(_('unsynced changes\n'))
955 954 return
956 955
957 956 fp.seek(0)
958 957
959 958 # send addchangegroup output to client
960 959
961 960 old_stdout = sys.stdout
962 961 sys.stdout = cStringIO.StringIO()
963 962
964 963 try:
965 964 url = 'remote:%s:%s' % (proto,
966 965 req.env.get('REMOTE_HOST', ''))
967 966 ret = self.repo.addchangegroup(fp, 'serve', url)
968 967 finally:
969 968 val = sys.stdout.getvalue()
970 969 sys.stdout = old_stdout
971 970 req.write('%d\n' % ret)
972 971 req.write(val)
973 972 finally:
974 973 lock.release()
975 974 finally:
976 975 fp.close()
977 976 os.unlink(tempname)
978 977
979 978 def do_stream_out(self, req):
980 979 req.httphdr("application/mercurial-0.1")
981 980 streamclone.stream_out(self.repo, req)
@@ -1,352 +1,351 b''
1 1 # httprepo.py - HTTP repository proxy classes for mercurial
2 2 #
3 3 # Copyright 2005, 2006 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 node import *
10 10 from remoterepo import *
11 11 from i18n import gettext as _
12 12 from demandload import *
13 13 demandload(globals(), "hg os urllib urllib2 urlparse zlib util httplib")
14 14 demandload(globals(), "errno keepalive tempfile socket")
15 15
16 16 class passwordmgr(urllib2.HTTPPasswordMgrWithDefaultRealm):
17 17 def __init__(self, ui):
18 18 urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
19 19 self.ui = ui
20 20
21 21 def find_user_password(self, realm, authuri):
22 22 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
23 23 self, realm, authuri)
24 24 user, passwd = authinfo
25 25 if user and passwd:
26 26 return (user, passwd)
27 27
28 28 if not self.ui.interactive:
29 29 raise util.Abort(_('http authorization required'))
30 30
31 31 self.ui.write(_("http authorization required\n"))
32 32 self.ui.status(_("realm: %s\n") % realm)
33 33 if user:
34 34 self.ui.status(_("user: %s\n") % user)
35 35 else:
36 36 user = self.ui.prompt(_("user:"), default=None)
37 37
38 38 if not passwd:
39 39 passwd = self.ui.getpass()
40 40
41 41 self.add_password(realm, authuri, user, passwd)
42 42 return (user, passwd)
43 43
44 44 def netlocsplit(netloc):
45 45 '''split [user[:passwd]@]host[:port] into 4-tuple.'''
46 46
47 47 a = netloc.find('@')
48 48 if a == -1:
49 49 user, passwd = None, None
50 50 else:
51 51 userpass, netloc = netloc[:a], netloc[a+1:]
52 52 c = userpass.find(':')
53 53 if c == -1:
54 54 user, passwd = urllib.unquote(userpass), None
55 55 else:
56 56 user = urllib.unquote(userpass[:c])
57 57 passwd = urllib.unquote(userpass[c+1:])
58 58 c = netloc.find(':')
59 59 if c == -1:
60 60 host, port = netloc, None
61 61 else:
62 62 host, port = netloc[:c], netloc[c+1:]
63 63 return host, port, user, passwd
64 64
65 65 def netlocunsplit(host, port, user=None, passwd=None):
66 66 '''turn host, port, user, passwd into [user[:passwd]@]host[:port].'''
67 67 if port:
68 68 hostport = host + ':' + port
69 69 else:
70 70 hostport = host
71 71 if user:
72 72 if passwd:
73 73 userpass = urllib.quote(user) + ':' + urllib.quote(passwd)
74 74 else:
75 75 userpass = urllib.quote(user)
76 76 return userpass + '@' + hostport
77 77 return hostport
78 78
79 79 class httpconnection(keepalive.HTTPConnection):
80 80 # must be able to send big bundle as stream.
81 81
82 82 def send(self, data):
83 83 if isinstance(data, str):
84 84 keepalive.HTTPConnection.send(self, data)
85 85 else:
86 86 # if auth required, some data sent twice, so rewind here
87 87 data.seek(0)
88 88 for chunk in util.filechunkiter(data):
89 89 keepalive.HTTPConnection.send(self, chunk)
90 90
91 91 class basehttphandler(keepalive.HTTPHandler):
92 92 def http_open(self, req):
93 93 return self.do_open(httpconnection, req)
94 94
95 95 has_https = hasattr(urllib2, 'HTTPSHandler')
96 96 if has_https:
97 97 class httpsconnection(httplib.HTTPSConnection):
98 98 response_class = keepalive.HTTPResponse
99 99 # must be able to send big bundle as stream.
100 100
101 101 def send(self, data):
102 102 if isinstance(data, str):
103 103 httplib.HTTPSConnection.send(self, data)
104 104 else:
105 105 # if auth required, some data sent twice, so rewind here
106 106 data.seek(0)
107 107 for chunk in util.filechunkiter(data):
108 108 httplib.HTTPSConnection.send(self, chunk)
109 109
110 110 class httphandler(basehttphandler, urllib2.HTTPSHandler):
111 111 def https_open(self, req):
112 112 return self.do_open(httpsconnection, req)
113 113 else:
114 114 class httphandler(basehttphandler):
115 115 pass
116 116
117 117 class httprepository(remoterepository):
118 118 def __init__(self, ui, path):
119 119 self.path = path
120 120 self.caps = None
121 121 scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path)
122 122 if query or frag:
123 123 raise util.Abort(_('unsupported URL component: "%s"') %
124 124 (query or frag))
125 125 if not urlpath: urlpath = '/'
126 126 host, port, user, passwd = netlocsplit(netloc)
127 127
128 128 # urllib cannot handle URLs with embedded user or passwd
129 129 self._url = urlparse.urlunsplit((scheme, netlocunsplit(host, port),
130 130 urlpath, '', ''))
131 131 self.ui = ui
132 132
133 133 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
134 proxyauthinfo = None
134 # XXX proxyauthinfo = None
135 135 handler = httphandler()
136 136
137 137 if proxyurl:
138 138 # proxy can be proper url or host[:port]
139 139 if not (proxyurl.startswith('http:') or
140 140 proxyurl.startswith('https:')):
141 141 proxyurl = 'http://' + proxyurl + '/'
142 142 snpqf = urlparse.urlsplit(proxyurl)
143 143 proxyscheme, proxynetloc, proxypath, proxyquery, proxyfrag = snpqf
144 144 hpup = netlocsplit(proxynetloc)
145 145
146 146 proxyhost, proxyport, proxyuser, proxypasswd = hpup
147 147 if not proxyuser:
148 148 proxyuser = ui.config("http_proxy", "user")
149 149 proxypasswd = ui.config("http_proxy", "passwd")
150 150
151 151 # see if we should use a proxy for this url
152 152 no_list = [ "localhost", "127.0.0.1" ]
153 153 no_list.extend([p.lower() for
154 154 p in ui.configlist("http_proxy", "no")])
155 155 no_list.extend([p.strip().lower() for
156 156 p in os.getenv("no_proxy", '').split(',')
157 157 if p.strip()])
158 158 # "http_proxy.always" config is for running tests on localhost
159 159 if (not ui.configbool("http_proxy", "always") and
160 160 host.lower() in no_list):
161 161 ui.debug(_('disabling proxy for %s\n') % host)
162 162 else:
163 163 proxyurl = urlparse.urlunsplit((
164 164 proxyscheme, netlocunsplit(proxyhost, proxyport,
165 165 proxyuser, proxypasswd or ''),
166 166 proxypath, proxyquery, proxyfrag))
167 167 handler = urllib2.ProxyHandler({scheme: proxyurl})
168 168 ui.debug(_('proxying through %s\n') % proxyurl)
169 169
170 170 # urllib2 takes proxy values from the environment and those
171 171 # will take precedence if found, so drop them
172 172 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
173 173 try:
174 174 if os.environ.has_key(env):
175 175 del os.environ[env]
176 176 except OSError:
177 177 pass
178 178
179 179 passmgr = passwordmgr(ui)
180 180 if user:
181 181 ui.debug(_('http auth: user %s, password %s\n') %
182 182 (user, passwd and '*' * len(passwd) or 'not set'))
183 183 passmgr.add_password(None, host, user, passwd or '')
184 184
185 185 opener = urllib2.build_opener(
186 186 handler,
187 187 urllib2.HTTPBasicAuthHandler(passmgr),
188 188 urllib2.HTTPDigestAuthHandler(passmgr))
189 189
190 190 # 1.0 here is the _protocol_ version
191 191 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
192 192 urllib2.install_opener(opener)
193 193
194 194 def url(self):
195 195 return self.path
196 196
197 197 # look up capabilities only when needed
198 198
199 199 def get_caps(self):
200 200 if self.caps is None:
201 201 try:
202 202 self.caps = self.do_read('capabilities').split()
203 203 except hg.RepoError:
204 204 self.caps = ()
205 205 self.ui.debug(_('capabilities: %s\n') %
206 206 (' '.join(self.caps or ['none'])))
207 207 return self.caps
208 208
209 209 capabilities = property(get_caps)
210 210
211 211 def lock(self):
212 212 raise util.Abort(_('operation not supported over http'))
213 213
214 214 def do_cmd(self, cmd, **args):
215 215 data = args.pop('data', None)
216 216 headers = args.pop('headers', {})
217 217 self.ui.debug(_("sending %s command\n") % cmd)
218 218 q = {"cmd": cmd}
219 219 q.update(args)
220 220 qs = urllib.urlencode(q)
221 221 cu = "%s?%s" % (self._url, qs)
222 222 try:
223 223 resp = urllib2.urlopen(urllib2.Request(cu, data, headers))
224 224 except urllib2.HTTPError, inst:
225 225 if inst.code == 401:
226 226 raise util.Abort(_('authorization failed'))
227 227 raise
228 228 except httplib.HTTPException, inst:
229 229 self.ui.debug(_('http error while sending %s command\n') % cmd)
230 230 self.ui.print_exc()
231 231 raise IOError(None, inst)
232 232 try:
233 233 proto = resp.getheader('content-type')
234 234 except AttributeError:
235 235 proto = resp.headers['content-type']
236 236
237 237 # accept old "text/plain" and "application/hg-changegroup" for now
238 238 if not proto.startswith('application/mercurial') and \
239 239 not proto.startswith('text/plain') and \
240 240 not proto.startswith('application/hg-changegroup'):
241 241 raise hg.RepoError(_("'%s' does not appear to be an hg repository") %
242 242 self._url)
243 243
244 244 if proto.startswith('application/mercurial'):
245 245 version = proto[22:]
246 246 if float(version) > 0.1:
247 247 raise hg.RepoError(_("'%s' uses newer protocol %s") %
248 248 (self._url, version))
249 249
250 250 return resp
251 251
252 252 def do_read(self, cmd, **args):
253 253 fp = self.do_cmd(cmd, **args)
254 254 try:
255 255 return fp.read()
256 256 finally:
257 257 # if using keepalive, allow connection to be reused
258 258 fp.close()
259 259
260 260 def heads(self):
261 261 d = self.do_read("heads")
262 262 try:
263 263 return map(bin, d[:-1].split(" "))
264 264 except:
265 265 self.ui.warn(_("unexpected response:\n") + d[:400] + "\n...\n")
266 266 raise
267 267
268 268 def branches(self, nodes):
269 269 n = " ".join(map(hex, nodes))
270 270 d = self.do_read("branches", nodes=n)
271 271 try:
272 272 br = [ tuple(map(bin, b.split(" "))) for b in d.splitlines() ]
273 273 return br
274 274 except:
275 275 self.ui.warn(_("unexpected response:\n") + d[:400] + "\n...\n")
276 276 raise
277 277
278 278 def between(self, pairs):
279 279 n = "\n".join(["-".join(map(hex, p)) for p in pairs])
280 280 d = self.do_read("between", pairs=n)
281 281 try:
282 282 p = [ l and map(bin, l.split(" ")) or [] for l in d.splitlines() ]
283 283 return p
284 284 except:
285 285 self.ui.warn(_("unexpected response:\n") + d[:400] + "\n...\n")
286 286 raise
287 287
288 288 def changegroup(self, nodes, kind):
289 289 n = " ".join(map(hex, nodes))
290 290 f = self.do_cmd("changegroup", roots=n)
291 bytes = 0
292 291
293 292 def zgenerator(f):
294 293 zd = zlib.decompressobj()
295 294 try:
296 295 for chnk in f:
297 296 yield zd.decompress(chnk)
298 except httplib.HTTPException, inst:
297 except httplib.HTTPException:
299 298 raise IOError(None, _('connection ended unexpectedly'))
300 299 yield zd.flush()
301 300
302 301 return util.chunkbuffer(zgenerator(util.filechunkiter(f)))
303 302
304 303 def unbundle(self, cg, heads, source):
305 304 # have to stream bundle to a temp file because we do not have
306 305 # http 1.1 chunked transfer.
307 306
308 307 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
309 308 fp = os.fdopen(fd, 'wb+')
310 309 try:
311 310 for chunk in util.filechunkiter(cg):
312 311 fp.write(chunk)
313 312 length = fp.tell()
314 313 try:
315 314 rfp = self.do_cmd(
316 315 'unbundle', data=fp,
317 316 headers={'content-length': length,
318 317 'content-type': 'application/octet-stream'},
319 318 heads=' '.join(map(hex, heads)))
320 319 try:
321 320 ret = int(rfp.readline())
322 321 self.ui.write(rfp.read())
323 322 return ret
324 323 finally:
325 324 rfp.close()
326 325 except socket.error, err:
327 326 if err[0] in (errno.ECONNRESET, errno.EPIPE):
328 327 raise util.Abort(_('push failed: %s') % err[1])
329 328 raise util.Abort(err[1])
330 329 finally:
331 330 fp.close()
332 331 os.unlink(tempname)
333 332
334 333 def stream_out(self):
335 334 return self.do_cmd('stream_out')
336 335
337 336 class httpsrepository(httprepository):
338 337 def __init__(self, ui, path):
339 338 if not has_https:
340 339 raise util.Abort(_('Python support for SSL and HTTPS '
341 340 'is not installed'))
342 341 httprepository.__init__(self, ui, path)
343 342
344 343 def instance(ui, path, create):
345 344 if create:
346 345 raise util.Abort(_('cannot create new http repository'))
347 346 if path.startswith('hg:'):
348 347 ui.warn(_("hg:// syntax is deprecated, please use http:// instead\n"))
349 348 path = 'http:' + path[3:]
350 349 if path.startswith('https:'):
351 350 return httpsrepository(ui, path)
352 351 return httprepository(ui, path)
@@ -1,1303 +1,1302 b''
1 1 """
2 2 revlog.py - storage back-end for mercurial
3 3
4 4 This provides efficient delta storage with O(1) retrieve and append
5 5 and O(changes) merge between branches
6 6
7 7 Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
8 8
9 9 This software may be used and distributed according to the terms
10 10 of the GNU General Public License, incorporated herein by reference.
11 11 """
12 12
13 13 from node import *
14 14 from i18n import gettext as _
15 15 from demandload import demandload
16 16 demandload(globals(), "binascii changegroup errno heapq mdiff os")
17 17 demandload(globals(), "sha struct util zlib")
18 18
19 19 # revlog version strings
20 20 REVLOGV0 = 0
21 21 REVLOGNG = 1
22 22
23 23 # revlog flags
24 24 REVLOGNGINLINEDATA = (1 << 16)
25 25 REVLOG_DEFAULT_FLAGS = REVLOGNGINLINEDATA
26 26
27 27 REVLOG_DEFAULT_FORMAT = REVLOGNG
28 28 REVLOG_DEFAULT_VERSION = REVLOG_DEFAULT_FORMAT | REVLOG_DEFAULT_FLAGS
29 29
30 30 def flagstr(flag):
31 31 if flag == "inline":
32 32 return REVLOGNGINLINEDATA
33 33 raise RevlogError(_("unknown revlog flag %s" % flag))
34 34
35 35 def hash(text, p1, p2):
36 36 """generate a hash from the given text and its parent hashes
37 37
38 38 This hash combines both the current file contents and its history
39 39 in a manner that makes it easy to distinguish nodes with the same
40 40 content in the revision graph.
41 41 """
42 42 l = [p1, p2]
43 43 l.sort()
44 44 s = sha.new(l[0])
45 45 s.update(l[1])
46 46 s.update(text)
47 47 return s.digest()
48 48
49 49 def compress(text):
50 50 """ generate a possibly-compressed representation of text """
51 51 if not text: return ("", text)
52 52 if len(text) < 44:
53 53 if text[0] == '\0': return ("", text)
54 54 return ('u', text)
55 55 bin = zlib.compress(text)
56 56 if len(bin) > len(text):
57 57 if text[0] == '\0': return ("", text)
58 58 return ('u', text)
59 59 return ("", bin)
60 60
61 61 def decompress(bin):
62 62 """ decompress the given input """
63 63 if not bin: return bin
64 64 t = bin[0]
65 65 if t == '\0': return bin
66 66 if t == 'x': return zlib.decompress(bin)
67 67 if t == 'u': return bin[1:]
68 68 raise RevlogError(_("unknown compression type %r") % t)
69 69
70 70 indexformatv0 = ">4l20s20s20s"
71 71 v0shaoffset = 56
72 72 # index ng:
73 73 # 6 bytes offset
74 74 # 2 bytes flags
75 75 # 4 bytes compressed length
76 76 # 4 bytes uncompressed length
77 77 # 4 bytes: base rev
78 78 # 4 bytes link rev
79 79 # 4 bytes parent 1 rev
80 80 # 4 bytes parent 2 rev
81 81 # 32 bytes: nodeid
82 82 indexformatng = ">Qiiiiii20s12x"
83 83 ngshaoffset = 32
84 84 versionformat = ">i"
85 85
86 86 class lazyparser(object):
87 87 """
88 88 this class avoids the need to parse the entirety of large indices
89 89 """
90 90
91 91 # lazyparser is not safe to use on windows if win32 extensions not
92 92 # available. it keeps file handle open, which make it not possible
93 93 # to break hardlinks on local cloned repos.
94 94 safe_to_use = os.name != 'nt' or (not util.is_win_9x() and
95 95 hasattr(util, 'win32api'))
96 96
97 97 def __init__(self, dataf, size, indexformat, shaoffset):
98 98 self.dataf = dataf
99 99 self.format = indexformat
100 100 self.s = struct.calcsize(indexformat)
101 101 self.indexformat = indexformat
102 102 self.datasize = size
103 103 self.l = size/self.s
104 104 self.index = [None] * self.l
105 105 self.map = {nullid: -1}
106 106 self.allmap = 0
107 107 self.all = 0
108 108 self.mapfind_count = 0
109 109 self.shaoffset = shaoffset
110 110
111 111 def loadmap(self):
112 112 """
113 113 during a commit, we need to make sure the rev being added is
114 114 not a duplicate. This requires loading the entire index,
115 115 which is fairly slow. loadmap can load up just the node map,
116 116 which takes much less time.
117 117 """
118 118 if self.allmap: return
119 start = 0
120 119 end = self.datasize
121 120 self.allmap = 1
122 121 cur = 0
123 122 count = 0
124 123 blocksize = self.s * 256
125 124 self.dataf.seek(0)
126 125 while cur < end:
127 126 data = self.dataf.read(blocksize)
128 127 off = 0
129 128 for x in xrange(256):
130 129 n = data[off + self.shaoffset:off + self.shaoffset + 20]
131 130 self.map[n] = count
132 131 count += 1
133 132 if count >= self.l:
134 133 break
135 134 off += self.s
136 135 cur += blocksize
137 136
138 137 def loadblock(self, blockstart, blocksize, data=None):
139 138 if self.all: return
140 139 if data is None:
141 140 self.dataf.seek(blockstart)
142 141 if blockstart + blocksize > self.datasize:
143 142 # the revlog may have grown since we've started running,
144 143 # but we don't have space in self.index for more entries.
145 144 # limit blocksize so that we don't get too much data.
146 145 blocksize = max(self.datasize - blockstart, 0)
147 146 data = self.dataf.read(blocksize)
148 147 lend = len(data) / self.s
149 148 i = blockstart / self.s
150 149 off = 0
151 150 for x in xrange(lend):
152 151 if self.index[i + x] == None:
153 152 b = data[off : off + self.s]
154 153 self.index[i + x] = b
155 154 n = b[self.shaoffset:self.shaoffset + 20]
156 155 self.map[n] = i + x
157 156 off += self.s
158 157
159 158 def findnode(self, node):
160 159 """search backwards through the index file for a specific node"""
161 160 if self.allmap: return None
162 161
163 162 # hg log will cause many many searches for the manifest
164 163 # nodes. After we get called a few times, just load the whole
165 164 # thing.
166 165 if self.mapfind_count > 8:
167 166 self.loadmap()
168 167 if node in self.map:
169 168 return node
170 169 return None
171 170 self.mapfind_count += 1
172 171 last = self.l - 1
173 172 while self.index[last] != None:
174 173 if last == 0:
175 174 self.all = 1
176 175 self.allmap = 1
177 176 return None
178 177 last -= 1
179 178 end = (last + 1) * self.s
180 179 blocksize = self.s * 256
181 180 while end >= 0:
182 181 start = max(end - blocksize, 0)
183 182 self.dataf.seek(start)
184 183 data = self.dataf.read(end - start)
185 184 findend = end - start
186 185 while True:
187 186 # we're searching backwards, so weh have to make sure
188 187 # we don't find a changeset where this node is a parent
189 188 off = data.rfind(node, 0, findend)
190 189 findend = off
191 190 if off >= 0:
192 191 i = off / self.s
193 192 off = i * self.s
194 193 n = data[off + self.shaoffset:off + self.shaoffset + 20]
195 194 if n == node:
196 195 self.map[n] = i + start / self.s
197 196 return node
198 197 else:
199 198 break
200 199 end -= blocksize
201 200 return None
202 201
203 202 def loadindex(self, i=None, end=None):
204 203 if self.all: return
205 204 all = False
206 205 if i == None:
207 206 blockstart = 0
208 207 blocksize = (512 / self.s) * self.s
209 208 end = self.datasize
210 209 all = True
211 210 else:
212 211 if end:
213 212 blockstart = i * self.s
214 213 end = end * self.s
215 214 blocksize = end - blockstart
216 215 else:
217 216 blockstart = (i & ~(32)) * self.s
218 217 blocksize = self.s * 64
219 218 end = blockstart + blocksize
220 219 while blockstart < end:
221 220 self.loadblock(blockstart, blocksize)
222 221 blockstart += blocksize
223 222 if all: self.all = True
224 223
225 224 class lazyindex(object):
226 225 """a lazy version of the index array"""
227 226 def __init__(self, parser):
228 227 self.p = parser
229 228 def __len__(self):
230 229 return len(self.p.index)
231 230 def load(self, pos):
232 231 if pos < 0:
233 232 pos += len(self.p.index)
234 233 self.p.loadindex(pos)
235 234 return self.p.index[pos]
236 235 def __getitem__(self, pos):
237 236 ret = self.p.index[pos] or self.load(pos)
238 237 if isinstance(ret, str):
239 238 ret = struct.unpack(self.p.indexformat, ret)
240 239 return ret
241 240 def __setitem__(self, pos, item):
242 241 self.p.index[pos] = item
243 242 def __delitem__(self, pos):
244 243 del self.p.index[pos]
245 244 def append(self, e):
246 245 self.p.index.append(e)
247 246
248 247 class lazymap(object):
249 248 """a lazy version of the node map"""
250 249 def __init__(self, parser):
251 250 self.p = parser
252 251 def load(self, key):
253 252 n = self.p.findnode(key)
254 253 if n == None:
255 254 raise KeyError(key)
256 255 def __contains__(self, key):
257 256 if key in self.p.map:
258 257 return True
259 258 self.p.loadmap()
260 259 return key in self.p.map
261 260 def __iter__(self):
262 261 yield nullid
263 262 for i in xrange(self.p.l):
264 263 ret = self.p.index[i]
265 264 if not ret:
266 265 self.p.loadindex(i)
267 266 ret = self.p.index[i]
268 267 if isinstance(ret, str):
269 268 ret = struct.unpack(self.p.indexformat, ret)
270 269 yield ret[-1]
271 270 def __getitem__(self, key):
272 271 try:
273 272 return self.p.map[key]
274 273 except KeyError:
275 274 try:
276 275 self.load(key)
277 276 return self.p.map[key]
278 277 except KeyError:
279 278 raise KeyError("node " + hex(key))
280 279 def __setitem__(self, key, val):
281 280 self.p.map[key] = val
282 281 def __delitem__(self, key):
283 282 del self.p.map[key]
284 283
285 284 class RevlogError(Exception): pass
286 285
287 286 class revlog(object):
288 287 """
289 288 the underlying revision storage object
290 289
291 290 A revlog consists of two parts, an index and the revision data.
292 291
293 292 The index is a file with a fixed record size containing
294 293 information on each revision, includings its nodeid (hash), the
295 294 nodeids of its parents, the position and offset of its data within
296 295 the data file, and the revision it's based on. Finally, each entry
297 296 contains a linkrev entry that can serve as a pointer to external
298 297 data.
299 298
300 299 The revision data itself is a linear collection of data chunks.
301 300 Each chunk represents a revision and is usually represented as a
302 301 delta against the previous chunk. To bound lookup time, runs of
303 302 deltas are limited to about 2 times the length of the original
304 303 version data. This makes retrieval of a version proportional to
305 304 its size, or O(1) relative to the number of revisions.
306 305
307 306 Both pieces of the revlog are written to in an append-only
308 307 fashion, which means we never need to rewrite a file to insert or
309 308 remove data, and can use some simple techniques to avoid the need
310 309 for locking while reading.
311 310 """
312 311 def __init__(self, opener, indexfile, datafile,
313 312 defversion=REVLOG_DEFAULT_VERSION):
314 313 """
315 314 create a revlog object
316 315
317 316 opener is a function that abstracts the file opening operation
318 317 and can be used to implement COW semantics or the like.
319 318 """
320 319 self.indexfile = indexfile
321 320 self.datafile = datafile
322 321 self.opener = opener
323 322
324 323 self.indexstat = None
325 324 self.cache = None
326 325 self.chunkcache = None
327 326 self.defversion = defversion
328 327 self.load()
329 328
330 329 def load(self):
331 330 v = self.defversion
332 331 try:
333 332 f = self.opener(self.indexfile)
334 333 i = f.read(4)
335 334 f.seek(0)
336 335 except IOError, inst:
337 336 if inst.errno != errno.ENOENT:
338 337 raise
339 338 i = ""
340 339 else:
341 340 try:
342 341 st = util.fstat(f)
343 342 except AttributeError, inst:
344 343 st = None
345 344 else:
346 345 oldst = self.indexstat
347 346 if (oldst and st.st_dev == oldst.st_dev
348 347 and st.st_ino == oldst.st_ino
349 348 and st.st_mtime == oldst.st_mtime
350 349 and st.st_ctime == oldst.st_ctime):
351 350 return
352 351 self.indexstat = st
353 352 if len(i) > 0:
354 353 v = struct.unpack(versionformat, i)[0]
355 354 flags = v & ~0xFFFF
356 355 fmt = v & 0xFFFF
357 356 if fmt == REVLOGV0:
358 357 if flags:
359 358 raise RevlogError(_("index %s invalid flags %x for format v0" %
360 359 (self.indexfile, flags)))
361 360 elif fmt == REVLOGNG:
362 361 if flags & ~REVLOGNGINLINEDATA:
363 362 raise RevlogError(_("index %s invalid flags %x for revlogng" %
364 363 (self.indexfile, flags)))
365 364 else:
366 365 raise RevlogError(_("index %s invalid format %d" %
367 366 (self.indexfile, fmt)))
368 367 self.version = v
369 368 if v == REVLOGV0:
370 369 self.indexformat = indexformatv0
371 370 shaoffset = v0shaoffset
372 371 else:
373 372 self.indexformat = indexformatng
374 373 shaoffset = ngshaoffset
375 374
376 375 if i:
377 376 if (lazyparser.safe_to_use and not self.inlinedata() and
378 377 st and st.st_size > 10000):
379 378 # big index, let's parse it on demand
380 379 parser = lazyparser(f, st.st_size, self.indexformat, shaoffset)
381 380 self.index = lazyindex(parser)
382 381 self.nodemap = lazymap(parser)
383 382 else:
384 383 self.parseindex(f, st)
385 384 if self.version != REVLOGV0:
386 385 e = list(self.index[0])
387 386 type = self.ngtype(e[0])
388 387 e[0] = self.offset_type(0, type)
389 388 self.index[0] = e
390 389 else:
391 390 self.nodemap = { nullid: -1}
392 391 self.index = []
393 392
394 393
395 394 def parseindex(self, fp, st):
396 395 s = struct.calcsize(self.indexformat)
397 396 self.index = []
398 397 self.nodemap = {nullid: -1}
399 398 inline = self.inlinedata()
400 399 n = 0
401 400 leftover = None
402 401 while True:
403 402 if st:
404 403 data = fp.read(65536)
405 404 else:
406 405 # hack for httprangereader, it doesn't do partial reads well
407 406 data = fp.read()
408 407 if not data:
409 408 break
410 409 if n == 0 and self.inlinedata():
411 410 # cache the first chunk
412 411 self.chunkcache = (0, data)
413 412 if leftover:
414 413 data = leftover + data
415 414 leftover = None
416 415 off = 0
417 416 l = len(data)
418 417 while off < l:
419 418 if l - off < s:
420 419 leftover = data[off:]
421 420 break
422 421 cur = data[off:off + s]
423 422 off += s
424 423 e = struct.unpack(self.indexformat, cur)
425 424 self.index.append(e)
426 425 self.nodemap[e[-1]] = n
427 426 n += 1
428 427 if inline:
429 428 off += e[1]
430 429 if off > l:
431 430 # some things don't seek well, just read it
432 431 fp.read(off - l)
433 432 if not st:
434 433 break
435 434
436 435
437 436 def ngoffset(self, q):
438 437 if q & 0xFFFF:
439 438 raise RevlogError(_('%s: incompatible revision flag %x') %
440 439 (self.indexfile, q))
441 440 return long(q >> 16)
442 441
443 442 def ngtype(self, q):
444 443 return int(q & 0xFFFF)
445 444
446 445 def offset_type(self, offset, type):
447 446 return long(long(offset) << 16 | type)
448 447
449 448 def loadindex(self, start, end):
450 449 """load a block of indexes all at once from the lazy parser"""
451 450 if isinstance(self.index, lazyindex):
452 451 self.index.p.loadindex(start, end)
453 452
454 453 def loadindexmap(self):
455 454 """loads both the map and the index from the lazy parser"""
456 455 if isinstance(self.index, lazyindex):
457 456 p = self.index.p
458 457 p.loadindex()
459 458 self.nodemap = p.map
460 459
461 460 def loadmap(self):
462 461 """loads the map from the lazy parser"""
463 462 if isinstance(self.nodemap, lazymap):
464 463 self.nodemap.p.loadmap()
465 464 self.nodemap = self.nodemap.p.map
466 465
467 466 def inlinedata(self): return self.version & REVLOGNGINLINEDATA
468 467 def tip(self): return self.node(len(self.index) - 1)
469 468 def count(self): return len(self.index)
470 469 def node(self, rev):
471 470 return (rev < 0) and nullid or self.index[rev][-1]
472 471 def rev(self, node):
473 472 try:
474 473 return self.nodemap[node]
475 474 except KeyError:
476 475 raise RevlogError(_('%s: no node %s') % (self.indexfile, hex(node)))
477 476 def linkrev(self, node):
478 477 return (node == nullid) and -1 or self.index[self.rev(node)][-4]
479 478 def parents(self, node):
480 479 if node == nullid: return (nullid, nullid)
481 480 r = self.rev(node)
482 481 d = self.index[r][-3:-1]
483 482 if self.version == REVLOGV0:
484 483 return d
485 484 return [ self.node(x) for x in d ]
486 485 def parentrevs(self, rev):
487 486 if rev == -1:
488 487 return (-1, -1)
489 488 d = self.index[rev][-3:-1]
490 489 if self.version == REVLOGV0:
491 490 return [ self.rev(x) for x in d ]
492 491 return d
493 492 def start(self, rev):
494 493 if rev < 0:
495 494 return -1
496 495 if self.version != REVLOGV0:
497 496 return self.ngoffset(self.index[rev][0])
498 497 return self.index[rev][0]
499 498
500 499 def end(self, rev): return self.start(rev) + self.length(rev)
501 500
502 501 def size(self, rev):
503 502 """return the length of the uncompressed text for a given revision"""
504 503 l = -1
505 504 if self.version != REVLOGV0:
506 505 l = self.index[rev][2]
507 506 if l >= 0:
508 507 return l
509 508
510 509 t = self.revision(self.node(rev))
511 510 return len(t)
512 511
513 512 # alternate implementation, The advantage to this code is it
514 513 # will be faster for a single revision. But, the results are not
515 514 # cached, so finding the size of every revision will be slower.
516 515 """
517 516 if self.cache and self.cache[1] == rev:
518 517 return len(self.cache[2])
519 518
520 519 base = self.base(rev)
521 520 if self.cache and self.cache[1] >= base and self.cache[1] < rev:
522 521 base = self.cache[1]
523 522 text = self.cache[2]
524 523 else:
525 524 text = self.revision(self.node(base))
526 525
527 526 l = len(text)
528 527 for x in xrange(base + 1, rev + 1):
529 528 l = mdiff.patchedsize(l, self.chunk(x))
530 529 return l
531 530 """
532 531
533 532 def length(self, rev):
534 533 if rev < 0:
535 534 return 0
536 535 else:
537 536 return self.index[rev][1]
538 537 def base(self, rev): return (rev < 0) and rev or self.index[rev][-5]
539 538
540 539 def reachable(self, rev, stop=None):
541 540 reachable = {}
542 541 visit = [rev]
543 542 reachable[rev] = 1
544 543 if stop:
545 544 stopn = self.rev(stop)
546 545 else:
547 546 stopn = 0
548 547 while visit:
549 548 n = visit.pop(0)
550 549 if n == stop:
551 550 continue
552 551 if n == nullid:
553 552 continue
554 553 for p in self.parents(n):
555 554 if self.rev(p) < stopn:
556 555 continue
557 556 if p not in reachable:
558 557 reachable[p] = 1
559 558 visit.append(p)
560 559 return reachable
561 560
562 561 def nodesbetween(self, roots=None, heads=None):
563 562 """Return a tuple containing three elements. Elements 1 and 2 contain
564 563 a final list bases and heads after all the unreachable ones have been
565 564 pruned. Element 0 contains a topologically sorted list of all
566 565
567 566 nodes that satisfy these constraints:
568 567 1. All nodes must be descended from a node in roots (the nodes on
569 568 roots are considered descended from themselves).
570 569 2. All nodes must also be ancestors of a node in heads (the nodes in
571 570 heads are considered to be their own ancestors).
572 571
573 572 If roots is unspecified, nullid is assumed as the only root.
574 573 If heads is unspecified, it is taken to be the output of the
575 574 heads method (i.e. a list of all nodes in the repository that
576 575 have no children)."""
577 576 nonodes = ([], [], [])
578 577 if roots is not None:
579 578 roots = list(roots)
580 579 if not roots:
581 580 return nonodes
582 581 lowestrev = min([self.rev(n) for n in roots])
583 582 else:
584 583 roots = [nullid] # Everybody's a descendent of nullid
585 584 lowestrev = -1
586 585 if (lowestrev == -1) and (heads is None):
587 586 # We want _all_ the nodes!
588 587 return ([self.node(r) for r in xrange(0, self.count())],
589 588 [nullid], list(self.heads()))
590 589 if heads is None:
591 590 # All nodes are ancestors, so the latest ancestor is the last
592 591 # node.
593 592 highestrev = self.count() - 1
594 593 # Set ancestors to None to signal that every node is an ancestor.
595 594 ancestors = None
596 595 # Set heads to an empty dictionary for later discovery of heads
597 596 heads = {}
598 597 else:
599 598 heads = list(heads)
600 599 if not heads:
601 600 return nonodes
602 601 ancestors = {}
603 602 # Start at the top and keep marking parents until we're done.
604 603 nodestotag = heads[:]
605 604 # Turn heads into a dictionary so we can remove 'fake' heads.
606 605 # Also, later we will be using it to filter out the heads we can't
607 606 # find from roots.
608 607 heads = dict.fromkeys(heads, 0)
609 608 # Remember where the top was so we can use it as a limit later.
610 609 highestrev = max([self.rev(n) for n in nodestotag])
611 610 while nodestotag:
612 611 # grab a node to tag
613 612 n = nodestotag.pop()
614 613 # Never tag nullid
615 614 if n == nullid:
616 615 continue
617 616 # A node's revision number represents its place in a
618 617 # topologically sorted list of nodes.
619 618 r = self.rev(n)
620 619 if r >= lowestrev:
621 620 if n not in ancestors:
622 621 # If we are possibly a descendent of one of the roots
623 622 # and we haven't already been marked as an ancestor
624 623 ancestors[n] = 1 # Mark as ancestor
625 624 # Add non-nullid parents to list of nodes to tag.
626 625 nodestotag.extend([p for p in self.parents(n) if
627 626 p != nullid])
628 627 elif n in heads: # We've seen it before, is it a fake head?
629 628 # So it is, real heads should not be the ancestors of
630 629 # any other heads.
631 630 heads.pop(n)
632 631 if not ancestors:
633 632 return nonodes
634 633 # Now that we have our set of ancestors, we want to remove any
635 634 # roots that are not ancestors.
636 635
637 636 # If one of the roots was nullid, everything is included anyway.
638 637 if lowestrev > -1:
639 638 # But, since we weren't, let's recompute the lowest rev to not
640 639 # include roots that aren't ancestors.
641 640
642 641 # Filter out roots that aren't ancestors of heads
643 642 roots = [n for n in roots if n in ancestors]
644 643 # Recompute the lowest revision
645 644 if roots:
646 645 lowestrev = min([self.rev(n) for n in roots])
647 646 else:
648 647 # No more roots? Return empty list
649 648 return nonodes
650 649 else:
651 650 # We are descending from nullid, and don't need to care about
652 651 # any other roots.
653 652 lowestrev = -1
654 653 roots = [nullid]
655 654 # Transform our roots list into a 'set' (i.e. a dictionary where the
656 655 # values don't matter.
657 656 descendents = dict.fromkeys(roots, 1)
658 657 # Also, keep the original roots so we can filter out roots that aren't
659 658 # 'real' roots (i.e. are descended from other roots).
660 659 roots = descendents.copy()
661 660 # Our topologically sorted list of output nodes.
662 661 orderedout = []
663 662 # Don't start at nullid since we don't want nullid in our output list,
664 663 # and if nullid shows up in descedents, empty parents will look like
665 664 # they're descendents.
666 665 for r in xrange(max(lowestrev, 0), highestrev + 1):
667 666 n = self.node(r)
668 667 isdescendent = False
669 668 if lowestrev == -1: # Everybody is a descendent of nullid
670 669 isdescendent = True
671 670 elif n in descendents:
672 671 # n is already a descendent
673 672 isdescendent = True
674 673 # This check only needs to be done here because all the roots
675 674 # will start being marked is descendents before the loop.
676 675 if n in roots:
677 676 # If n was a root, check if it's a 'real' root.
678 677 p = tuple(self.parents(n))
679 678 # If any of its parents are descendents, it's not a root.
680 679 if (p[0] in descendents) or (p[1] in descendents):
681 680 roots.pop(n)
682 681 else:
683 682 p = tuple(self.parents(n))
684 683 # A node is a descendent if either of its parents are
685 684 # descendents. (We seeded the dependents list with the roots
686 685 # up there, remember?)
687 686 if (p[0] in descendents) or (p[1] in descendents):
688 687 descendents[n] = 1
689 688 isdescendent = True
690 689 if isdescendent and ((ancestors is None) or (n in ancestors)):
691 690 # Only include nodes that are both descendents and ancestors.
692 691 orderedout.append(n)
693 692 if (ancestors is not None) and (n in heads):
694 693 # We're trying to figure out which heads are reachable
695 694 # from roots.
696 695 # Mark this head as having been reached
697 696 heads[n] = 1
698 697 elif ancestors is None:
699 698 # Otherwise, we're trying to discover the heads.
700 699 # Assume this is a head because if it isn't, the next step
701 700 # will eventually remove it.
702 701 heads[n] = 1
703 702 # But, obviously its parents aren't.
704 703 for p in self.parents(n):
705 704 heads.pop(p, None)
706 705 heads = [n for n in heads.iterkeys() if heads[n] != 0]
707 706 roots = roots.keys()
708 707 assert orderedout
709 708 assert roots
710 709 assert heads
711 710 return (orderedout, roots, heads)
712 711
713 712 def heads(self, start=None):
714 713 """return the list of all nodes that have no children
715 714
716 715 if start is specified, only heads that are descendants of
717 716 start will be returned
718 717
719 718 """
720 719 if start is None:
721 720 start = nullid
722 721 startrev = self.rev(start)
723 722 reachable = {startrev: 1}
724 723 heads = {startrev: 1}
725 724
726 725 parentrevs = self.parentrevs
727 726 for r in xrange(startrev + 1, self.count()):
728 727 for p in parentrevs(r):
729 728 if p in reachable:
730 729 reachable[r] = 1
731 730 heads[r] = 1
732 731 if p in heads:
733 732 del heads[p]
734 733 return [self.node(r) for r in heads]
735 734
736 735 def children(self, node):
737 736 """find the children of a given node"""
738 737 c = []
739 738 p = self.rev(node)
740 739 for r in range(p + 1, self.count()):
741 740 n = self.node(r)
742 741 for pn in self.parents(n):
743 742 if pn == node:
744 743 c.append(n)
745 744 continue
746 745 elif pn == nullid:
747 746 continue
748 747 return c
749 748
750 749 def lookup(self, id):
751 750 """locate a node based on revision number or subset of hex nodeid"""
752 751 if type(id) == type(0):
753 752 return self.node(id)
754 753 try:
755 754 rev = int(id)
756 755 if str(rev) != id: raise ValueError
757 756 if rev < 0: rev = self.count() + rev
758 757 if rev < 0 or rev >= self.count(): raise ValueError
759 758 return self.node(rev)
760 759 except (ValueError, OverflowError):
761 760 c = []
762 761 for n in self.nodemap:
763 762 if hex(n).startswith(id):
764 763 c.append(n)
765 764 if len(c) > 1: raise RevlogError(_("Ambiguous identifier"))
766 765 if len(c) == 1: return c[0]
767 766
768 767 # might need fixing if we change hash lengths
769 768 if len(id) == 20 and id in self.nodemap:
770 769 return id
771 770
772 771 raise RevlogError(_("No match found"))
773 772
774 773 def cmp(self, node, text):
775 774 """compare text with a given file revision"""
776 775 p1, p2 = self.parents(node)
777 776 return hash(text, p1, p2) != node
778 777
779 778 def makenode(self, node, text):
780 779 """calculate a file nodeid for text, descended or possibly
781 780 unchanged from node"""
782 781
783 782 if self.cmp(node, text):
784 783 return hash(text, node, nullid)
785 784 return node
786 785
787 786 def diff(self, a, b):
788 787 """return a delta between two revisions"""
789 788 return mdiff.textdiff(a, b)
790 789
791 790 def patches(self, t, pl):
792 791 """apply a list of patches to a string"""
793 792 return mdiff.patches(t, pl)
794 793
795 794 def chunk(self, rev, df=None, cachelen=4096):
796 795 start, length = self.start(rev), self.length(rev)
797 796 inline = self.inlinedata()
798 797 if inline:
799 798 start += (rev + 1) * struct.calcsize(self.indexformat)
800 799 end = start + length
801 800 def loadcache(df):
802 801 cache_length = max(cachelen, length) # 4k
803 802 if not df:
804 803 if inline:
805 804 df = self.opener(self.indexfile)
806 805 else:
807 806 df = self.opener(self.datafile)
808 807 df.seek(start)
809 808 self.chunkcache = (start, df.read(cache_length))
810 809
811 810 if not self.chunkcache:
812 811 loadcache(df)
813 812
814 813 cache_start = self.chunkcache[0]
815 814 cache_end = cache_start + len(self.chunkcache[1])
816 815 if start >= cache_start and end <= cache_end:
817 816 # it is cached
818 817 offset = start - cache_start
819 818 else:
820 819 loadcache(df)
821 820 offset = 0
822 821
823 822 #def checkchunk():
824 823 # df = self.opener(self.datafile)
825 824 # df.seek(start)
826 825 # return df.read(length)
827 826 #assert s == checkchunk()
828 827 return decompress(self.chunkcache[1][offset:offset + length])
829 828
830 829 def delta(self, node):
831 830 """return or calculate a delta between a node and its predecessor"""
832 831 r = self.rev(node)
833 832 return self.revdiff(r - 1, r)
834 833
835 834 def revdiff(self, rev1, rev2):
836 835 """return or calculate a delta between two revisions"""
837 836 b1 = self.base(rev1)
838 837 b2 = self.base(rev2)
839 838 if b1 == b2 and rev1 + 1 == rev2:
840 839 return self.chunk(rev2)
841 840 else:
842 841 return self.diff(self.revision(self.node(rev1)),
843 842 self.revision(self.node(rev2)))
844 843
845 844 def revision(self, node):
846 845 """return an uncompressed revision of a given"""
847 846 if node == nullid: return ""
848 847 if self.cache and self.cache[0] == node: return self.cache[2]
849 848
850 849 # look up what we need to read
851 850 text = None
852 851 rev = self.rev(node)
853 852 base = self.base(rev)
854 853
855 854 if self.inlinedata():
856 855 # we probably have the whole chunk cached
857 856 df = None
858 857 else:
859 858 df = self.opener(self.datafile)
860 859
861 860 # do we have useful data cached?
862 861 if self.cache and self.cache[1] >= base and self.cache[1] < rev:
863 862 base = self.cache[1]
864 863 text = self.cache[2]
865 864 self.loadindex(base, rev + 1)
866 865 else:
867 866 self.loadindex(base, rev + 1)
868 867 text = self.chunk(base, df=df)
869 868
870 869 bins = []
871 870 for r in xrange(base + 1, rev + 1):
872 871 bins.append(self.chunk(r, df=df))
873 872
874 873 text = self.patches(text, bins)
875 874
876 875 p1, p2 = self.parents(node)
877 876 if node != hash(text, p1, p2):
878 877 raise RevlogError(_("integrity check failed on %s:%d")
879 878 % (self.datafile, rev))
880 879
881 880 self.cache = (node, rev, text)
882 881 return text
883 882
884 883 def checkinlinesize(self, tr, fp=None):
885 884 if not self.inlinedata():
886 885 return
887 886 if not fp:
888 887 fp = self.opener(self.indexfile, 'r')
889 888 fp.seek(0, 2)
890 889 size = fp.tell()
891 890 if size < 131072:
892 891 return
893 892 trinfo = tr.find(self.indexfile)
894 893 if trinfo == None:
895 894 raise RevlogError(_("%s not found in the transaction" %
896 895 self.indexfile))
897 896
898 897 trindex = trinfo[2]
899 898 dataoff = self.start(trindex)
900 899
901 900 tr.add(self.datafile, dataoff)
902 901 df = self.opener(self.datafile, 'w')
903 902 calc = struct.calcsize(self.indexformat)
904 903 for r in xrange(self.count()):
905 904 start = self.start(r) + (r + 1) * calc
906 905 length = self.length(r)
907 906 fp.seek(start)
908 907 d = fp.read(length)
909 908 df.write(d)
910 909 fp.close()
911 910 df.close()
912 911 fp = self.opener(self.indexfile, 'w', atomictemp=True)
913 912 self.version &= ~(REVLOGNGINLINEDATA)
914 913 if self.count():
915 914 x = self.index[0]
916 915 e = struct.pack(self.indexformat, *x)[4:]
917 916 l = struct.pack(versionformat, self.version)
918 917 fp.write(l)
919 918 fp.write(e)
920 919
921 920 for i in xrange(1, self.count()):
922 921 x = self.index[i]
923 922 e = struct.pack(self.indexformat, *x)
924 923 fp.write(e)
925 924
926 925 # if we don't call rename, the temp file will never replace the
927 926 # real index
928 927 fp.rename()
929 928
930 929 tr.replace(self.indexfile, trindex * calc)
931 930 self.chunkcache = None
932 931
933 932 def addrevision(self, text, transaction, link, p1=None, p2=None, d=None):
934 933 """add a revision to the log
935 934
936 935 text - the revision data to add
937 936 transaction - the transaction object used for rollback
938 937 link - the linkrev data to add
939 938 p1, p2 - the parent nodeids of the revision
940 939 d - an optional precomputed delta
941 940 """
942 941 if text is None: text = ""
943 942 if p1 is None: p1 = self.tip()
944 943 if p2 is None: p2 = nullid
945 944
946 945 node = hash(text, p1, p2)
947 946
948 947 if node in self.nodemap:
949 948 return node
950 949
951 950 n = self.count()
952 951 t = n - 1
953 952
954 953 if n:
955 954 base = self.base(t)
956 955 start = self.start(base)
957 956 end = self.end(t)
958 957 if not d:
959 958 prev = self.revision(self.tip())
960 959 d = self.diff(prev, str(text))
961 960 data = compress(d)
962 961 l = len(data[1]) + len(data[0])
963 962 dist = end - start + l
964 963
965 964 # full versions are inserted when the needed deltas
966 965 # become comparable to the uncompressed text
967 966 if not n or dist > len(text) * 2:
968 967 data = compress(text)
969 968 l = len(data[1]) + len(data[0])
970 969 base = n
971 970 else:
972 971 base = self.base(t)
973 972
974 973 offset = 0
975 974 if t >= 0:
976 975 offset = self.end(t)
977 976
978 977 if self.version == REVLOGV0:
979 978 e = (offset, l, base, link, p1, p2, node)
980 979 else:
981 980 e = (self.offset_type(offset, 0), l, len(text),
982 981 base, link, self.rev(p1), self.rev(p2), node)
983 982
984 983 self.index.append(e)
985 984 self.nodemap[node] = n
986 985 entry = struct.pack(self.indexformat, *e)
987 986
988 987 if not self.inlinedata():
989 988 transaction.add(self.datafile, offset)
990 989 transaction.add(self.indexfile, n * len(entry))
991 990 f = self.opener(self.datafile, "a")
992 991 if data[0]:
993 992 f.write(data[0])
994 993 f.write(data[1])
995 994 f.close()
996 995 f = self.opener(self.indexfile, "a")
997 996 else:
998 997 f = self.opener(self.indexfile, "a+")
999 998 f.seek(0, 2)
1000 999 transaction.add(self.indexfile, f.tell(), self.count() - 1)
1001 1000
1002 1001 if len(self.index) == 1 and self.version != REVLOGV0:
1003 1002 l = struct.pack(versionformat, self.version)
1004 1003 f.write(l)
1005 1004 entry = entry[4:]
1006 1005
1007 1006 f.write(entry)
1008 1007
1009 1008 if self.inlinedata():
1010 1009 f.write(data[0])
1011 1010 f.write(data[1])
1012 1011 self.checkinlinesize(transaction, f)
1013 1012
1014 1013 self.cache = (node, n, text)
1015 1014 return node
1016 1015
1017 1016 def ancestor(self, a, b):
1018 1017 """calculate the least common ancestor of nodes a and b"""
1019 1018
1020 1019 # start with some short cuts for the linear cases
1021 1020 if a == b:
1022 1021 return a
1023 1022 ra = self.rev(a)
1024 1023 rb = self.rev(b)
1025 1024 if ra < rb:
1026 1025 last = b
1027 1026 first = a
1028 1027 else:
1029 1028 last = a
1030 1029 first = b
1031 1030
1032 1031 # reachable won't include stop in the list, so we have to use a parent
1033 1032 reachable = self.reachable(last, stop=self.parents(first)[0])
1034 1033 if first in reachable:
1035 1034 return first
1036 1035
1037 1036 # calculate the distance of every node from root
1038 1037 dist = {nullid: 0}
1039 1038 for i in xrange(self.count()):
1040 1039 n = self.node(i)
1041 1040 p1, p2 = self.parents(n)
1042 1041 dist[n] = max(dist[p1], dist[p2]) + 1
1043 1042
1044 1043 # traverse ancestors in order of decreasing distance from root
1045 1044 def ancestors(node):
1046 1045 # we store negative distances because heap returns smallest member
1047 1046 h = [(-dist[node], node)]
1048 1047 seen = {}
1049 1048 while h:
1050 1049 d, n = heapq.heappop(h)
1051 1050 if n not in seen:
1052 1051 seen[n] = 1
1053 1052 yield (-d, n)
1054 1053 for p in self.parents(n):
1055 1054 heapq.heappush(h, (-dist[p], p))
1056 1055
1057 1056 def generations(node):
1058 1057 sg, s = None, {}
1059 1058 for g,n in ancestors(node):
1060 1059 if g != sg:
1061 1060 if sg:
1062 1061 yield sg, s
1063 1062 sg, s = g, {n:1}
1064 1063 else:
1065 1064 s[n] = 1
1066 1065 yield sg, s
1067 1066
1068 1067 x = generations(a)
1069 1068 y = generations(b)
1070 1069 gx = x.next()
1071 1070 gy = y.next()
1072 1071
1073 1072 # increment each ancestor list until it is closer to root than
1074 1073 # the other, or they match
1075 1074 while 1:
1076 1075 #print "ancestor gen %s %s" % (gx[0], gy[0])
1077 1076 if gx[0] == gy[0]:
1078 1077 # find the intersection
1079 1078 i = [ n for n in gx[1] if n in gy[1] ]
1080 1079 if i:
1081 1080 return i[0]
1082 1081 else:
1083 1082 #print "next"
1084 1083 gy = y.next()
1085 1084 gx = x.next()
1086 1085 elif gx[0] < gy[0]:
1087 1086 #print "next y"
1088 1087 gy = y.next()
1089 1088 else:
1090 1089 #print "next x"
1091 1090 gx = x.next()
1092 1091
1093 1092 def group(self, nodelist, lookup, infocollect=None):
1094 1093 """calculate a delta group
1095 1094
1096 1095 Given a list of changeset revs, return a set of deltas and
1097 1096 metadata corresponding to nodes. the first delta is
1098 1097 parent(nodes[0]) -> nodes[0] the receiver is guaranteed to
1099 1098 have this parent as it has all history before these
1100 1099 changesets. parent is parent[0]
1101 1100 """
1102 1101 revs = [self.rev(n) for n in nodelist]
1103 1102
1104 1103 # if we don't have any revisions touched by these changesets, bail
1105 1104 if not revs:
1106 1105 yield changegroup.closechunk()
1107 1106 return
1108 1107
1109 1108 # add the parent of the first rev
1110 1109 p = self.parents(self.node(revs[0]))[0]
1111 1110 revs.insert(0, self.rev(p))
1112 1111
1113 1112 # build deltas
1114 1113 for d in xrange(0, len(revs) - 1):
1115 1114 a, b = revs[d], revs[d + 1]
1116 1115 nb = self.node(b)
1117 1116
1118 1117 if infocollect is not None:
1119 1118 infocollect(nb)
1120 1119
1121 1120 d = self.revdiff(a, b)
1122 1121 p = self.parents(nb)
1123 1122 meta = nb + p[0] + p[1] + lookup(nb)
1124 1123 yield changegroup.genchunk("%s%s" % (meta, d))
1125 1124
1126 1125 yield changegroup.closechunk()
1127 1126
1128 1127 def addgroup(self, revs, linkmapper, transaction, unique=0):
1129 1128 """
1130 1129 add a delta group
1131 1130
1132 1131 given a set of deltas, add them to the revision log. the
1133 1132 first delta is against its parent, which should be in our
1134 1133 log, the rest are against the previous delta.
1135 1134 """
1136 1135
1137 1136 #track the base of the current delta log
1138 1137 r = self.count()
1139 1138 t = r - 1
1140 1139 node = None
1141 1140
1142 1141 base = prev = -1
1143 1142 start = end = textlen = 0
1144 1143 if r:
1145 1144 end = self.end(t)
1146 1145
1147 1146 ifh = self.opener(self.indexfile, "a+")
1148 1147 ifh.seek(0, 2)
1149 1148 transaction.add(self.indexfile, ifh.tell(), self.count())
1150 1149 if self.inlinedata():
1151 1150 dfh = None
1152 1151 else:
1153 1152 transaction.add(self.datafile, end)
1154 1153 dfh = self.opener(self.datafile, "a")
1155 1154
1156 1155 # loop through our set of deltas
1157 1156 chain = None
1158 1157 for chunk in revs:
1159 1158 node, p1, p2, cs = struct.unpack("20s20s20s20s", chunk[:80])
1160 1159 link = linkmapper(cs)
1161 1160 if node in self.nodemap:
1162 1161 # this can happen if two branches make the same change
1163 1162 # if unique:
1164 1163 # raise RevlogError(_("already have %s") % hex(node[:4]))
1165 1164 chain = node
1166 1165 continue
1167 1166 delta = chunk[80:]
1168 1167
1169 1168 for p in (p1, p2):
1170 1169 if not p in self.nodemap:
1171 1170 raise RevlogError(_("unknown parent %s") % short(p))
1172 1171
1173 1172 if not chain:
1174 1173 # retrieve the parent revision of the delta chain
1175 1174 chain = p1
1176 1175 if not chain in self.nodemap:
1177 1176 raise RevlogError(_("unknown base %s") % short(chain[:4]))
1178 1177
1179 1178 # full versions are inserted when the needed deltas become
1180 1179 # comparable to the uncompressed text or when the previous
1181 1180 # version is not the one we have a delta against. We use
1182 1181 # the size of the previous full rev as a proxy for the
1183 1182 # current size.
1184 1183
1185 1184 if chain == prev:
1186 1185 tempd = compress(delta)
1187 1186 cdelta = tempd[0] + tempd[1]
1188 1187 textlen = mdiff.patchedsize(textlen, delta)
1189 1188
1190 1189 if chain != prev or (end - start + len(cdelta)) > textlen * 2:
1191 1190 # flush our writes here so we can read it in revision
1192 1191 if dfh:
1193 1192 dfh.flush()
1194 1193 ifh.flush()
1195 1194 text = self.revision(chain)
1196 1195 text = self.patches(text, [delta])
1197 1196 chk = self.addrevision(text, transaction, link, p1, p2)
1198 1197 if chk != node:
1199 1198 raise RevlogError(_("consistency error adding group"))
1200 1199 textlen = len(text)
1201 1200 else:
1202 1201 if self.version == REVLOGV0:
1203 1202 e = (end, len(cdelta), base, link, p1, p2, node)
1204 1203 else:
1205 1204 e = (self.offset_type(end, 0), len(cdelta), textlen, base,
1206 1205 link, self.rev(p1), self.rev(p2), node)
1207 1206 self.index.append(e)
1208 1207 self.nodemap[node] = r
1209 1208 if self.inlinedata():
1210 1209 ifh.write(struct.pack(self.indexformat, *e))
1211 1210 ifh.write(cdelta)
1212 1211 self.checkinlinesize(transaction, ifh)
1213 1212 if not self.inlinedata():
1214 1213 dfh = self.opener(self.datafile, "a")
1215 1214 ifh = self.opener(self.indexfile, "a")
1216 1215 else:
1217 1216 if not dfh:
1218 1217 # addrevision switched from inline to conventional
1219 1218 # reopen the index
1220 1219 dfh = self.opener(self.datafile, "a")
1221 1220 ifh = self.opener(self.indexfile, "a")
1222 1221 dfh.write(cdelta)
1223 1222 ifh.write(struct.pack(self.indexformat, *e))
1224 1223
1225 1224 t, r, chain, prev = r, r + 1, node, node
1226 1225 base = self.base(t)
1227 1226 start = self.start(base)
1228 1227 end = self.end(t)
1229 1228
1230 1229 return node
1231 1230
1232 1231 def strip(self, rev, minlink):
1233 1232 if self.count() == 0 or rev >= self.count():
1234 1233 return
1235 1234
1236 1235 if isinstance(self.index, lazyindex):
1237 1236 self.loadindexmap()
1238 1237
1239 1238 # When stripping away a revision, we need to make sure it
1240 1239 # does not actually belong to an older changeset.
1241 1240 # The minlink parameter defines the oldest revision
1242 1241 # we're allowed to strip away.
1243 1242 while minlink > self.index[rev][-4]:
1244 1243 rev += 1
1245 1244 if rev >= self.count():
1246 1245 return
1247 1246
1248 1247 # first truncate the files on disk
1249 1248 end = self.start(rev)
1250 1249 if not self.inlinedata():
1251 1250 df = self.opener(self.datafile, "a")
1252 1251 df.truncate(end)
1253 1252 end = rev * struct.calcsize(self.indexformat)
1254 1253 else:
1255 1254 end += rev * struct.calcsize(self.indexformat)
1256 1255
1257 1256 indexf = self.opener(self.indexfile, "a")
1258 1257 indexf.truncate(end)
1259 1258
1260 1259 # then reset internal state in memory to forget those revisions
1261 1260 self.cache = None
1262 1261 self.chunkcache = None
1263 1262 for x in xrange(rev, self.count()):
1264 1263 del self.nodemap[self.node(x)]
1265 1264
1266 1265 del self.index[rev:]
1267 1266
1268 1267 def checksize(self):
1269 1268 expected = 0
1270 1269 if self.count():
1271 1270 expected = self.end(self.count() - 1)
1272 1271
1273 1272 try:
1274 1273 f = self.opener(self.datafile)
1275 1274 f.seek(0, 2)
1276 1275 actual = f.tell()
1277 1276 dd = actual - expected
1278 1277 except IOError, inst:
1279 1278 if inst.errno != errno.ENOENT:
1280 1279 raise
1281 1280 dd = 0
1282 1281
1283 1282 try:
1284 1283 f = self.opener(self.indexfile)
1285 1284 f.seek(0, 2)
1286 1285 actual = f.tell()
1287 1286 s = struct.calcsize(self.indexformat)
1288 1287 i = actual / s
1289 1288 di = actual - (i * s)
1290 1289 if self.inlinedata():
1291 1290 databytes = 0
1292 1291 for r in xrange(self.count()):
1293 1292 databytes += self.length(r)
1294 1293 dd = 0
1295 1294 di = actual - self.count() * s - databytes
1296 1295 except IOError, inst:
1297 1296 if inst.errno != errno.ENOENT:
1298 1297 raise
1299 1298 di = 0
1300 1299
1301 1300 return (dd, di)
1302 1301
1303 1302
@@ -1,997 +1,998 b''
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 Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
6 6 Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
7 7
8 8 This software may be used and distributed according to the terms
9 9 of the GNU General Public License, incorporated herein by reference.
10 10
11 11 This contains helper routines that are independent of the SCM core and hide
12 12 platform-specific details from the core.
13 13 """
14 14
15 15 from i18n import gettext as _
16 16 from demandload import *
17 17 demandload(globals(), "cStringIO errno getpass popen2 re shutil sys tempfile")
18 18 demandload(globals(), "os threading time")
19 19
20 20 # used by parsedate
21 21 defaultdateformats = ('%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M',
22 22 '%a %b %d %H:%M:%S %Y')
23 23
24 24 class SignalInterrupt(Exception):
25 25 """Exception raised on SIGTERM and SIGHUP."""
26 26
27 27 def pipefilter(s, cmd):
28 28 '''filter string S through command CMD, returning its output'''
29 29 (pout, pin) = popen2.popen2(cmd, -1, 'b')
30 30 def writer():
31 31 try:
32 32 pin.write(s)
33 33 pin.close()
34 34 except IOError, inst:
35 35 if inst.errno != errno.EPIPE:
36 36 raise
37 37
38 38 # we should use select instead on UNIX, but this will work on most
39 39 # systems, including Windows
40 40 w = threading.Thread(target=writer)
41 41 w.start()
42 42 f = pout.read()
43 43 pout.close()
44 44 w.join()
45 45 return f
46 46
47 47 def tempfilter(s, cmd):
48 48 '''filter string S through a pair of temporary files with CMD.
49 49 CMD is used as a template to create the real command to be run,
50 50 with the strings INFILE and OUTFILE replaced by the real names of
51 51 the temporary files generated.'''
52 52 inname, outname = None, None
53 53 try:
54 54 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
55 55 fp = os.fdopen(infd, 'wb')
56 56 fp.write(s)
57 57 fp.close()
58 58 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
59 59 os.close(outfd)
60 60 cmd = cmd.replace('INFILE', inname)
61 61 cmd = cmd.replace('OUTFILE', outname)
62 62 code = os.system(cmd)
63 63 if code: raise Abort(_("command '%s' failed: %s") %
64 64 (cmd, explain_exit(code)))
65 65 return open(outname, 'rb').read()
66 66 finally:
67 67 try:
68 68 if inname: os.unlink(inname)
69 69 except: pass
70 70 try:
71 71 if outname: os.unlink(outname)
72 72 except: pass
73 73
74 74 filtertable = {
75 75 'tempfile:': tempfilter,
76 76 'pipe:': pipefilter,
77 77 }
78 78
79 79 def filter(s, cmd):
80 80 "filter a string through a command that transforms its input to its output"
81 81 for name, fn in filtertable.iteritems():
82 82 if cmd.startswith(name):
83 83 return fn(s, cmd[len(name):].lstrip())
84 84 return pipefilter(s, cmd)
85 85
86 86 def find_in_path(name, path, default=None):
87 87 '''find name in search path. path can be string (will be split
88 88 with os.pathsep), or iterable thing that returns strings. if name
89 89 found, return path to name. else return default.'''
90 90 if isinstance(path, str):
91 91 path = path.split(os.pathsep)
92 92 for p in path:
93 93 p_name = os.path.join(p, name)
94 94 if os.path.exists(p_name):
95 95 return p_name
96 96 return default
97 97
98 98 def binary(s):
99 99 """return true if a string is binary data using diff's heuristic"""
100 100 if s and '\0' in s[:4096]:
101 101 return True
102 102 return False
103 103
104 104 def unique(g):
105 105 """return the uniq elements of iterable g"""
106 106 seen = {}
107 107 for f in g:
108 108 if f not in seen:
109 109 seen[f] = 1
110 110 yield f
111 111
112 112 class Abort(Exception):
113 113 """Raised if a command needs to print an error and exit."""
114 114
115 115 def always(fn): return True
116 116 def never(fn): return False
117 117
118 118 def patkind(name, dflt_pat='glob'):
119 119 """Split a string into an optional pattern kind prefix and the
120 120 actual pattern."""
121 121 for prefix in 're', 'glob', 'path', 'relglob', 'relpath', 'relre':
122 122 if name.startswith(prefix + ':'): return name.split(':', 1)
123 123 return dflt_pat, name
124 124
125 125 def globre(pat, head='^', tail='$'):
126 126 "convert a glob pattern into a regexp"
127 127 i, n = 0, len(pat)
128 128 res = ''
129 129 group = False
130 130 def peek(): return i < n and pat[i]
131 131 while i < n:
132 132 c = pat[i]
133 133 i = i+1
134 134 if c == '*':
135 135 if peek() == '*':
136 136 i += 1
137 137 res += '.*'
138 138 else:
139 139 res += '[^/]*'
140 140 elif c == '?':
141 141 res += '.'
142 142 elif c == '[':
143 143 j = i
144 144 if j < n and pat[j] in '!]':
145 145 j += 1
146 146 while j < n and pat[j] != ']':
147 147 j += 1
148 148 if j >= n:
149 149 res += '\\['
150 150 else:
151 151 stuff = pat[i:j].replace('\\','\\\\')
152 152 i = j + 1
153 153 if stuff[0] == '!':
154 154 stuff = '^' + stuff[1:]
155 155 elif stuff[0] == '^':
156 156 stuff = '\\' + stuff
157 157 res = '%s[%s]' % (res, stuff)
158 158 elif c == '{':
159 159 group = True
160 160 res += '(?:'
161 161 elif c == '}' and group:
162 162 res += ')'
163 163 group = False
164 164 elif c == ',' and group:
165 165 res += '|'
166 166 elif c == '\\':
167 167 p = peek()
168 168 if p:
169 169 i += 1
170 170 res += re.escape(p)
171 171 else:
172 172 res += re.escape(c)
173 173 else:
174 174 res += re.escape(c)
175 175 return head + res + tail
176 176
177 177 _globchars = {'[': 1, '{': 1, '*': 1, '?': 1}
178 178
179 179 def pathto(n1, n2):
180 180 '''return the relative path from one place to another.
181 181 this returns a path in the form used by the local filesystem, not hg.'''
182 182 if not n1: return localpath(n2)
183 183 a, b = n1.split('/'), n2.split('/')
184 184 a.reverse()
185 185 b.reverse()
186 186 while a and b and a[-1] == b[-1]:
187 187 a.pop()
188 188 b.pop()
189 189 b.reverse()
190 190 return os.sep.join((['..'] * len(a)) + b)
191 191
192 192 def canonpath(root, cwd, myname):
193 193 """return the canonical path of myname, given cwd and root"""
194 194 if root == os.sep:
195 195 rootsep = os.sep
196 196 elif root.endswith(os.sep):
197 197 rootsep = root
198 198 else:
199 199 rootsep = root + os.sep
200 200 name = myname
201 201 if not os.path.isabs(name):
202 202 name = os.path.join(root, cwd, name)
203 203 name = os.path.normpath(name)
204 204 if name != rootsep and name.startswith(rootsep):
205 205 name = name[len(rootsep):]
206 206 audit_path(name)
207 207 return pconvert(name)
208 208 elif name == root:
209 209 return ''
210 210 else:
211 211 # Determine whether `name' is in the hierarchy at or beneath `root',
212 212 # by iterating name=dirname(name) until that causes no change (can't
213 213 # check name == '/', because that doesn't work on windows). For each
214 214 # `name', compare dev/inode numbers. If they match, the list `rel'
215 215 # holds the reversed list of components making up the relative file
216 216 # name we want.
217 217 root_st = os.stat(root)
218 218 rel = []
219 219 while True:
220 220 try:
221 221 name_st = os.stat(name)
222 222 except OSError:
223 223 break
224 224 if samestat(name_st, root_st):
225 225 rel.reverse()
226 226 name = os.path.join(*rel)
227 227 audit_path(name)
228 228 return pconvert(name)
229 229 dirname, basename = os.path.split(name)
230 230 rel.append(basename)
231 231 if dirname == name:
232 232 break
233 233 name = dirname
234 234
235 235 raise Abort('%s not under root' % myname)
236 236
237 237 def matcher(canonroot, cwd='', names=['.'], inc=[], exc=[], head='', src=None):
238 238 return _matcher(canonroot, cwd, names, inc, exc, head, 'glob', src)
239 239
240 240 def cmdmatcher(canonroot, cwd='', names=['.'], inc=[], exc=[], head='', src=None):
241 241 if os.name == 'nt':
242 242 dflt_pat = 'glob'
243 243 else:
244 244 dflt_pat = 'relpath'
245 245 return _matcher(canonroot, cwd, names, inc, exc, head, dflt_pat, src)
246 246
247 247 def _matcher(canonroot, cwd, names, inc, exc, head, dflt_pat, src):
248 248 """build a function to match a set of file patterns
249 249
250 250 arguments:
251 251 canonroot - the canonical root of the tree you're matching against
252 252 cwd - the current working directory, if relevant
253 253 names - patterns to find
254 254 inc - patterns to include
255 255 exc - patterns to exclude
256 256 head - a regex to prepend to patterns to control whether a match is rooted
257 257
258 258 a pattern is one of:
259 259 'glob:<rooted glob>'
260 260 're:<rooted regexp>'
261 261 'path:<rooted path>'
262 262 'relglob:<relative glob>'
263 263 'relpath:<relative path>'
264 264 'relre:<relative regexp>'
265 265 '<rooted path or regexp>'
266 266
267 267 returns:
268 268 a 3-tuple containing
269 269 - list of explicit non-pattern names passed in
270 270 - a bool match(filename) function
271 271 - a bool indicating if any patterns were passed in
272 272
273 273 todo:
274 274 make head regex a rooted bool
275 275 """
276 276
277 277 def contains_glob(name):
278 278 for c in name:
279 279 if c in _globchars: return True
280 280 return False
281 281
282 282 def regex(kind, name, tail):
283 283 '''convert a pattern into a regular expression'''
284 284 if kind == 're':
285 285 return name
286 286 elif kind == 'path':
287 287 return '^' + re.escape(name) + '(?:/|$)'
288 288 elif kind == 'relglob':
289 289 return head + globre(name, '(?:|.*/)', tail)
290 290 elif kind == 'relpath':
291 291 return head + re.escape(name) + tail
292 292 elif kind == 'relre':
293 293 if name.startswith('^'):
294 294 return name
295 295 return '.*' + name
296 296 return head + globre(name, '', tail)
297 297
298 298 def matchfn(pats, tail):
299 299 """build a matching function from a set of patterns"""
300 300 if not pats:
301 301 return
302 302 matches = []
303 303 for k, p in pats:
304 304 try:
305 305 pat = '(?:%s)' % regex(k, p, tail)
306 306 matches.append(re.compile(pat).match)
307 307 except re.error:
308 308 if src: raise Abort("%s: invalid pattern (%s): %s" % (src, k, p))
309 309 else: raise Abort("invalid pattern (%s): %s" % (k, p))
310 310
311 311 def buildfn(text):
312 312 for m in matches:
313 313 r = m(text)
314 314 if r:
315 315 return r
316 316
317 317 return buildfn
318 318
319 319 def globprefix(pat):
320 320 '''return the non-glob prefix of a path, e.g. foo/* -> foo'''
321 321 root = []
322 322 for p in pat.split(os.sep):
323 323 if contains_glob(p): break
324 324 root.append(p)
325 325 return '/'.join(root)
326 326
327 327 pats = []
328 328 files = []
329 329 roots = []
330 330 for kind, name in [patkind(p, dflt_pat) for p in names]:
331 331 if kind in ('glob', 'relpath'):
332 332 name = canonpath(canonroot, cwd, name)
333 333 if name == '':
334 334 kind, name = 'glob', '**'
335 335 if kind in ('glob', 'path', 're'):
336 336 pats.append((kind, name))
337 337 if kind == 'glob':
338 338 root = globprefix(name)
339 339 if root: roots.append(root)
340 340 elif kind == 'relpath':
341 341 files.append((kind, name))
342 342 roots.append(name)
343 343
344 344 patmatch = matchfn(pats, '$') or always
345 345 filematch = matchfn(files, '(?:/|$)') or always
346 346 incmatch = always
347 347 if inc:
348 348 inckinds = [patkind(canonpath(canonroot, cwd, i)) for i in inc]
349 349 incmatch = matchfn(inckinds, '(?:/|$)')
350 350 excmatch = lambda fn: False
351 351 if exc:
352 352 exckinds = [patkind(canonpath(canonroot, cwd, x)) for x in exc]
353 353 excmatch = matchfn(exckinds, '(?:/|$)')
354 354
355 355 return (roots,
356 356 lambda fn: (incmatch(fn) and not excmatch(fn) and
357 357 (fn.endswith('/') or
358 358 (not pats and not files) or
359 359 (pats and patmatch(fn)) or
360 360 (files and filematch(fn)))),
361 361 (inc or exc or (pats and pats != [('glob', '**')])) and True)
362 362
363 363 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None):
364 364 '''enhanced shell command execution.
365 365 run with environment maybe modified, maybe in different dir.
366 366
367 367 if command fails and onerr is None, return status. if ui object,
368 368 print error message and return status, else raise onerr object as
369 369 exception.'''
370 370 def py2shell(val):
371 371 'convert python object into string that is useful to shell'
372 372 if val in (None, False):
373 373 return '0'
374 374 if val == True:
375 375 return '1'
376 376 return str(val)
377 377 oldenv = {}
378 378 for k in environ:
379 379 oldenv[k] = os.environ.get(k)
380 380 if cwd is not None:
381 381 oldcwd = os.getcwd()
382 382 try:
383 383 for k, v in environ.iteritems():
384 384 os.environ[k] = py2shell(v)
385 385 if cwd is not None and oldcwd != cwd:
386 386 os.chdir(cwd)
387 387 rc = os.system(cmd)
388 388 if rc and onerr:
389 389 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
390 390 explain_exit(rc)[0])
391 391 if errprefix:
392 392 errmsg = '%s: %s' % (errprefix, errmsg)
393 393 try:
394 394 onerr.warn(errmsg + '\n')
395 395 except AttributeError:
396 396 raise onerr(errmsg)
397 397 return rc
398 398 finally:
399 399 for k, v in oldenv.iteritems():
400 400 if v is None:
401 401 del os.environ[k]
402 402 else:
403 403 os.environ[k] = v
404 404 if cwd is not None and oldcwd != cwd:
405 405 os.chdir(oldcwd)
406 406
407 407 def rename(src, dst):
408 408 """forcibly rename a file"""
409 409 try:
410 410 os.rename(src, dst)
411 411 except OSError, err:
412 412 # on windows, rename to existing file is not allowed, so we
413 413 # must delete destination first. but if file is open, unlink
414 414 # schedules it for delete but does not delete it. rename
415 415 # happens immediately even for open files, so we create
416 416 # temporary file, delete it, rename destination to that name,
417 417 # then delete that. then rename is safe to do.
418 418 fd, temp = tempfile.mkstemp(dir=os.path.dirname(dst) or '.')
419 419 os.close(fd)
420 420 os.unlink(temp)
421 421 os.rename(dst, temp)
422 422 os.unlink(temp)
423 423 os.rename(src, dst)
424 424
425 425 def unlink(f):
426 426 """unlink and remove the directory if it is empty"""
427 427 os.unlink(f)
428 428 # try removing directories that might now be empty
429 429 try:
430 430 os.removedirs(os.path.dirname(f))
431 431 except OSError:
432 432 pass
433 433
434 434 def copyfiles(src, dst, hardlink=None):
435 435 """Copy a directory tree using hardlinks if possible"""
436 436
437 437 if hardlink is None:
438 438 hardlink = (os.stat(src).st_dev ==
439 439 os.stat(os.path.dirname(dst)).st_dev)
440 440
441 441 if os.path.isdir(src):
442 442 os.mkdir(dst)
443 443 for name in os.listdir(src):
444 444 srcname = os.path.join(src, name)
445 445 dstname = os.path.join(dst, name)
446 446 copyfiles(srcname, dstname, hardlink)
447 447 else:
448 448 if hardlink:
449 449 try:
450 450 os_link(src, dst)
451 451 except (IOError, OSError):
452 452 hardlink = False
453 453 shutil.copy(src, dst)
454 454 else:
455 455 shutil.copy(src, dst)
456 456
457 457 def audit_path(path):
458 458 """Abort if path contains dangerous components"""
459 459 parts = os.path.normcase(path).split(os.sep)
460 460 if (os.path.splitdrive(path)[0] or parts[0] in ('.hg', '')
461 461 or os.pardir in parts):
462 462 raise Abort(_("path contains illegal component: %s\n") % path)
463 463
464 464 def _makelock_file(info, pathname):
465 465 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
466 466 os.write(ld, info)
467 467 os.close(ld)
468 468
469 469 def _readlock_file(pathname):
470 470 return posixfile(pathname).read()
471 471
472 472 def nlinks(pathname):
473 473 """Return number of hardlinks for the given file."""
474 474 return os.lstat(pathname).st_nlink
475 475
476 476 if hasattr(os, 'link'):
477 477 os_link = os.link
478 478 else:
479 479 def os_link(src, dst):
480 480 raise OSError(0, _("Hardlinks not supported"))
481 481
482 482 def fstat(fp):
483 483 '''stat file object that may not have fileno method.'''
484 484 try:
485 485 return os.fstat(fp.fileno())
486 486 except AttributeError:
487 487 return os.stat(fp.name)
488 488
489 489 posixfile = file
490 490
491 491 def is_win_9x():
492 492 '''return true if run on windows 95, 98 or me.'''
493 493 try:
494 494 return sys.getwindowsversion()[3] == 1
495 495 except AttributeError:
496 496 return os.name == 'nt' and 'command' in os.environ.get('comspec', '')
497 497
498 498 getuser_fallback = None
499 499
500 500 def getuser():
501 501 '''return name of current user'''
502 502 try:
503 503 return getpass.getuser()
504 504 except ImportError:
505 505 # import of pwd will fail on windows - try fallback
506 506 if getuser_fallback:
507 507 return getuser_fallback()
508 508 # raised if win32api not available
509 509 raise Abort(_('user name not available - set USERNAME '
510 510 'environment variable'))
511 511
512 512 # Platform specific variants
513 513 if os.name == 'nt':
514 514 demandload(globals(), "msvcrt")
515 515 nulldev = 'NUL:'
516 516
517 517 class winstdout:
518 518 '''stdout on windows misbehaves if sent through a pipe'''
519 519
520 520 def __init__(self, fp):
521 521 self.fp = fp
522 522
523 523 def __getattr__(self, key):
524 524 return getattr(self.fp, key)
525 525
526 526 def close(self):
527 527 try:
528 528 self.fp.close()
529 529 except: pass
530 530
531 531 def write(self, s):
532 532 try:
533 533 return self.fp.write(s)
534 534 except IOError, inst:
535 535 if inst.errno != 0: raise
536 536 self.close()
537 537 raise IOError(errno.EPIPE, 'Broken pipe')
538 538
539 539 sys.stdout = winstdout(sys.stdout)
540 540
541 541 def system_rcpath():
542 542 try:
543 543 return system_rcpath_win32()
544 544 except:
545 545 return [r'c:\mercurial\mercurial.ini']
546 546
547 547 def os_rcpath():
548 548 '''return default os-specific hgrc search path'''
549 549 path = system_rcpath()
550 550 path.append(user_rcpath())
551 551 userprofile = os.environ.get('USERPROFILE')
552 552 if userprofile:
553 553 path.append(os.path.join(userprofile, 'mercurial.ini'))
554 554 return path
555 555
556 556 def user_rcpath():
557 557 '''return os-specific hgrc search path to the user dir'''
558 558 return os.path.join(os.path.expanduser('~'), 'mercurial.ini')
559 559
560 560 def parse_patch_output(output_line):
561 561 """parses the output produced by patch and returns the file name"""
562 562 pf = output_line[14:]
563 563 if pf[0] == '`':
564 564 pf = pf[1:-1] # Remove the quotes
565 565 return pf
566 566
567 567 def testpid(pid):
568 568 '''return False if pid dead, True if running or not known'''
569 569 return True
570 570
571 571 def is_exec(f, last):
572 572 return last
573 573
574 574 def set_exec(f, mode):
575 575 pass
576 576
577 577 def set_binary(fd):
578 578 msvcrt.setmode(fd.fileno(), os.O_BINARY)
579 579
580 580 def pconvert(path):
581 581 return path.replace("\\", "/")
582 582
583 583 def localpath(path):
584 584 return path.replace('/', '\\')
585 585
586 586 def normpath(path):
587 587 return pconvert(os.path.normpath(path))
588 588
589 589 makelock = _makelock_file
590 590 readlock = _readlock_file
591 591
592 592 def samestat(s1, s2):
593 593 return False
594 594
595 595 def shellquote(s):
596 596 return '"%s"' % s.replace('"', '\\"')
597 597
598 598 def explain_exit(code):
599 599 return _("exited with status %d") % code, code
600 600
601 601 try:
602 602 # override functions with win32 versions if possible
603 603 from util_win32 import *
604 604 if not is_win_9x():
605 605 posixfile = posixfile_nt
606 606 except ImportError:
607 607 pass
608 608
609 609 else:
610 610 nulldev = '/dev/null'
611 611
612 612 def rcfiles(path):
613 613 rcs = [os.path.join(path, 'hgrc')]
614 614 rcdir = os.path.join(path, 'hgrc.d')
615 615 try:
616 616 rcs.extend([os.path.join(rcdir, f) for f in os.listdir(rcdir)
617 617 if f.endswith(".rc")])
618 except OSError, inst: pass
618 except OSError:
619 pass
619 620 return rcs
620 621
621 622 def os_rcpath():
622 623 '''return default os-specific hgrc search path'''
623 624 path = []
624 625 # old mod_python does not set sys.argv
625 626 if len(getattr(sys, 'argv', [])) > 0:
626 627 path.extend(rcfiles(os.path.dirname(sys.argv[0]) +
627 628 '/../etc/mercurial'))
628 629 path.extend(rcfiles('/etc/mercurial'))
629 630 path.append(os.path.expanduser('~/.hgrc'))
630 631 path = [os.path.normpath(f) for f in path]
631 632 return path
632 633
633 634 def parse_patch_output(output_line):
634 635 """parses the output produced by patch and returns the file name"""
635 636 pf = output_line[14:]
636 637 if pf.startswith("'") and pf.endswith("'") and " " in pf:
637 638 pf = pf[1:-1] # Remove the quotes
638 639 return pf
639 640
640 641 def is_exec(f, last):
641 642 """check whether a file is executable"""
642 643 return (os.lstat(f).st_mode & 0100 != 0)
643 644
644 645 def set_exec(f, mode):
645 646 s = os.lstat(f).st_mode
646 647 if (s & 0100 != 0) == mode:
647 648 return
648 649 if mode:
649 650 # Turn on +x for every +r bit when making a file executable
650 651 # and obey umask.
651 652 umask = os.umask(0)
652 653 os.umask(umask)
653 654 os.chmod(f, s | (s & 0444) >> 2 & ~umask)
654 655 else:
655 656 os.chmod(f, s & 0666)
656 657
657 658 def set_binary(fd):
658 659 pass
659 660
660 661 def pconvert(path):
661 662 return path
662 663
663 664 def localpath(path):
664 665 return path
665 666
666 667 normpath = os.path.normpath
667 668 samestat = os.path.samestat
668 669
669 670 def makelock(info, pathname):
670 671 try:
671 672 os.symlink(info, pathname)
672 673 except OSError, why:
673 674 if why.errno == errno.EEXIST:
674 675 raise
675 676 else:
676 677 _makelock_file(info, pathname)
677 678
678 679 def readlock(pathname):
679 680 try:
680 681 return os.readlink(pathname)
681 682 except OSError, why:
682 683 if why.errno == errno.EINVAL:
683 684 return _readlock_file(pathname)
684 685 else:
685 686 raise
686 687
687 688 def shellquote(s):
688 689 return "'%s'" % s.replace("'", "'\\''")
689 690
690 691 def testpid(pid):
691 692 '''return False if pid dead, True if running or not sure'''
692 693 try:
693 694 os.kill(pid, 0)
694 695 return True
695 696 except OSError, inst:
696 697 return inst.errno != errno.ESRCH
697 698
698 699 def explain_exit(code):
699 700 """return a 2-tuple (desc, code) describing a process's status"""
700 701 if os.WIFEXITED(code):
701 702 val = os.WEXITSTATUS(code)
702 703 return _("exited with status %d") % val, val
703 704 elif os.WIFSIGNALED(code):
704 705 val = os.WTERMSIG(code)
705 706 return _("killed by signal %d") % val, val
706 707 elif os.WIFSTOPPED(code):
707 708 val = os.WSTOPSIG(code)
708 709 return _("stopped by signal %d") % val, val
709 710 raise ValueError(_("invalid exit code"))
710 711
711 712 def opener(base, audit=True):
712 713 """
713 714 return a function that opens files relative to base
714 715
715 716 this function is used to hide the details of COW semantics and
716 717 remote file access from higher level code.
717 718 """
718 719 p = base
719 720 audit_p = audit
720 721
721 722 def mktempcopy(name):
722 723 d, fn = os.path.split(name)
723 724 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
724 725 os.close(fd)
725 726 ofp = posixfile(temp, "wb")
726 727 try:
727 728 try:
728 729 ifp = posixfile(name, "rb")
729 730 except IOError, inst:
730 731 if not getattr(inst, 'filename', None):
731 732 inst.filename = name
732 733 raise
733 734 for chunk in filechunkiter(ifp):
734 735 ofp.write(chunk)
735 736 ifp.close()
736 737 ofp.close()
737 738 except:
738 739 try: os.unlink(temp)
739 740 except: pass
740 741 raise
741 742 st = os.lstat(name)
742 743 os.chmod(temp, st.st_mode)
743 744 return temp
744 745
745 746 class atomictempfile(posixfile):
746 747 """the file will only be copied when rename is called"""
747 748 def __init__(self, name, mode):
748 749 self.__name = name
749 750 self.temp = mktempcopy(name)
750 751 posixfile.__init__(self, self.temp, mode)
751 752 def rename(self):
752 753 if not self.closed:
753 754 posixfile.close(self)
754 755 rename(self.temp, localpath(self.__name))
755 756 def __del__(self):
756 757 if not self.closed:
757 758 try:
758 759 os.unlink(self.temp)
759 760 except: pass
760 761 posixfile.close(self)
761 762
762 763 class atomicfile(atomictempfile):
763 764 """the file will only be copied on close"""
764 765 def __init__(self, name, mode):
765 766 atomictempfile.__init__(self, name, mode)
766 767 def close(self):
767 768 self.rename()
768 769 def __del__(self):
769 770 self.rename()
770 771
771 772 def o(path, mode="r", text=False, atomic=False, atomictemp=False):
772 773 if audit_p:
773 774 audit_path(path)
774 775 f = os.path.join(p, path)
775 776
776 777 if not text:
777 778 mode += "b" # for that other OS
778 779
779 780 if mode[0] != "r":
780 781 try:
781 782 nlink = nlinks(f)
782 783 except OSError:
783 784 d = os.path.dirname(f)
784 785 if not os.path.isdir(d):
785 786 os.makedirs(d)
786 787 else:
787 788 if atomic:
788 789 return atomicfile(f, mode)
789 790 elif atomictemp:
790 791 return atomictempfile(f, mode)
791 792 if nlink > 1:
792 793 rename(mktempcopy(f), f)
793 794 return posixfile(f, mode)
794 795
795 796 return o
796 797
797 798 class chunkbuffer(object):
798 799 """Allow arbitrary sized chunks of data to be efficiently read from an
799 800 iterator over chunks of arbitrary size."""
800 801
801 802 def __init__(self, in_iter, targetsize = 2**16):
802 803 """in_iter is the iterator that's iterating over the input chunks.
803 804 targetsize is how big a buffer to try to maintain."""
804 805 self.in_iter = iter(in_iter)
805 806 self.buf = ''
806 807 self.targetsize = int(targetsize)
807 808 if self.targetsize <= 0:
808 809 raise ValueError(_("targetsize must be greater than 0, was %d") %
809 810 targetsize)
810 811 self.iterempty = False
811 812
812 813 def fillbuf(self):
813 814 """Ignore target size; read every chunk from iterator until empty."""
814 815 if not self.iterempty:
815 816 collector = cStringIO.StringIO()
816 817 collector.write(self.buf)
817 818 for ch in self.in_iter:
818 819 collector.write(ch)
819 820 self.buf = collector.getvalue()
820 821 self.iterempty = True
821 822
822 823 def read(self, l):
823 824 """Read L bytes of data from the iterator of chunks of data.
824 825 Returns less than L bytes if the iterator runs dry."""
825 826 if l > len(self.buf) and not self.iterempty:
826 827 # Clamp to a multiple of self.targetsize
827 828 targetsize = self.targetsize * ((l // self.targetsize) + 1)
828 829 collector = cStringIO.StringIO()
829 830 collector.write(self.buf)
830 831 collected = len(self.buf)
831 832 for chunk in self.in_iter:
832 833 collector.write(chunk)
833 834 collected += len(chunk)
834 835 if collected >= targetsize:
835 836 break
836 837 if collected < targetsize:
837 838 self.iterempty = True
838 839 self.buf = collector.getvalue()
839 840 s, self.buf = self.buf[:l], buffer(self.buf, l)
840 841 return s
841 842
842 843 def filechunkiter(f, size=65536, limit=None):
843 844 """Create a generator that produces the data in the file size
844 845 (default 65536) bytes at a time, up to optional limit (default is
845 846 to read all data). Chunks may be less than size bytes if the
846 847 chunk is the last chunk in the file, or the file is a socket or
847 848 some other type of file that sometimes reads less data than is
848 849 requested."""
849 850 assert size >= 0
850 851 assert limit is None or limit >= 0
851 852 while True:
852 853 if limit is None: nbytes = size
853 854 else: nbytes = min(limit, size)
854 855 s = nbytes and f.read(nbytes)
855 856 if not s: break
856 857 if limit: limit -= len(s)
857 858 yield s
858 859
859 860 def makedate():
860 861 lt = time.localtime()
861 862 if lt[8] == 1 and time.daylight:
862 863 tz = time.altzone
863 864 else:
864 865 tz = time.timezone
865 866 return time.mktime(lt), tz
866 867
867 868 def datestr(date=None, format='%a %b %d %H:%M:%S %Y', timezone=True):
868 869 """represent a (unixtime, offset) tuple as a localized time.
869 870 unixtime is seconds since the epoch, and offset is the time zone's
870 871 number of seconds away from UTC. if timezone is false, do not
871 872 append time zone to string."""
872 873 t, tz = date or makedate()
873 874 s = time.strftime(format, time.gmtime(float(t) - tz))
874 875 if timezone:
875 876 s += " %+03d%02d" % (-tz / 3600, ((-tz % 3600) / 60))
876 877 return s
877 878
878 879 def strdate(string, format='%a %b %d %H:%M:%S %Y'):
879 880 """parse a localized time string and return a (unixtime, offset) tuple.
880 881 if the string cannot be parsed, ValueError is raised."""
881 882 def hastimezone(string):
882 883 return (string[-4:].isdigit() and
883 884 (string[-5] == '+' or string[-5] == '-') and
884 885 string[-6].isspace())
885 886
886 887 if hastimezone(string):
887 888 date, tz = string[:-6], string[-5:]
888 889 tz = int(tz)
889 890 offset = - 3600 * (tz / 100) - 60 * (tz % 100)
890 891 else:
891 892 date, offset = string, 0
892 893 when = int(time.mktime(time.strptime(date, format))) + offset
893 894 return when, offset
894 895
895 896 def parsedate(string, formats=None):
896 897 """parse a localized time string and return a (unixtime, offset) tuple.
897 898 The date may be a "unixtime offset" string or in one of the specified
898 899 formats."""
899 900 if not formats:
900 901 formats = defaultdateformats
901 902 try:
902 903 when, offset = map(int, string.split(' '))
903 904 except ValueError:
904 905 for format in formats:
905 906 try:
906 907 when, offset = strdate(string, format)
907 908 except ValueError:
908 909 pass
909 910 else:
910 911 break
911 912 else:
912 913 raise ValueError(_('invalid date: %r') % string)
913 914 # validate explicit (probably user-specified) date and
914 915 # time zone offset. values must fit in signed 32 bits for
915 916 # current 32-bit linux runtimes. timezones go from UTC-12
916 917 # to UTC+14
917 918 if abs(when) > 0x7fffffff:
918 919 raise ValueError(_('date exceeds 32 bits: %d') % when)
919 920 if offset < -50400 or offset > 43200:
920 921 raise ValueError(_('impossible time zone offset: %d') % offset)
921 922 return when, offset
922 923
923 924 def shortuser(user):
924 925 """Return a short representation of a user name or email address."""
925 926 f = user.find('@')
926 927 if f >= 0:
927 928 user = user[:f]
928 929 f = user.find('<')
929 930 if f >= 0:
930 931 user = user[f+1:]
931 932 return user
932 933
933 934 def walkrepos(path):
934 935 '''yield every hg repository under path, recursively.'''
935 936 def errhandler(err):
936 937 if err.filename == path:
937 938 raise err
938 939
939 940 for root, dirs, files in os.walk(path, onerror=errhandler):
940 941 for d in dirs:
941 942 if d == '.hg':
942 943 yield root
943 944 dirs[:] = []
944 945 break
945 946
946 947 _rcpath = None
947 948
948 949 def rcpath():
949 950 '''return hgrc search path. if env var HGRCPATH is set, use it.
950 951 for each item in path, if directory, use files ending in .rc,
951 952 else use item.
952 953 make HGRCPATH empty to only look in .hg/hgrc of current repo.
953 954 if no HGRCPATH, use default os-specific path.'''
954 955 global _rcpath
955 956 if _rcpath is None:
956 957 if 'HGRCPATH' in os.environ:
957 958 _rcpath = []
958 959 for p in os.environ['HGRCPATH'].split(os.pathsep):
959 960 if not p: continue
960 961 if os.path.isdir(p):
961 962 for f in os.listdir(p):
962 963 if f.endswith('.rc'):
963 964 _rcpath.append(os.path.join(p, f))
964 965 else:
965 966 _rcpath.append(p)
966 967 else:
967 968 _rcpath = os_rcpath()
968 969 return _rcpath
969 970
970 971 def bytecount(nbytes):
971 972 '''return byte count formatted as readable string, with units'''
972 973
973 974 units = (
974 975 (100, 1<<30, _('%.0f GB')),
975 976 (10, 1<<30, _('%.1f GB')),
976 977 (1, 1<<30, _('%.2f GB')),
977 978 (100, 1<<20, _('%.0f MB')),
978 979 (10, 1<<20, _('%.1f MB')),
979 980 (1, 1<<20, _('%.2f MB')),
980 981 (100, 1<<10, _('%.0f KB')),
981 982 (10, 1<<10, _('%.1f KB')),
982 983 (1, 1<<10, _('%.2f KB')),
983 984 (1, 1, _('%.0f bytes')),
984 985 )
985 986
986 987 for multiplier, divisor, format in units:
987 988 if nbytes >= divisor * multiplier:
988 989 return format % (nbytes / float(divisor))
989 990 return units[-1][2] % nbytes
990 991
991 992 def drop_scheme(scheme, path):
992 993 sc = scheme + ':'
993 994 if path.startswith(sc):
994 995 path = path[len(sc):]
995 996 if path.startswith('//'):
996 997 path = path[2:]
997 998 return path
General Comments 0
You need to be logged in to leave comments. Login now