##// END OF EJS Templates
subrepos: be smarter about what's an absolute path (issue2808)
Matt Mackall -
r14766:4f56b753 stable
parent child Browse files
Show More
@@ -1,1085 +1,1085 b''
1 1 # subrepo.py - sub-repository handling for Mercurial
2 2 #
3 3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import errno, os, re, xml.dom.minidom, shutil, posixpath
9 9 import stat, subprocess, tarfile
10 10 from i18n import _
11 11 import config, scmutil, util, node, error, cmdutil, bookmarks
12 12 hg = None
13 13 propertycache = util.propertycache
14 14
15 15 nullstate = ('', '', 'empty')
16 16
17 17 def state(ctx, ui):
18 18 """return a state dict, mapping subrepo paths configured in .hgsub
19 19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
20 20 (key in types dict))
21 21 """
22 22 p = config.config()
23 23 def read(f, sections=None, remap=None):
24 24 if f in ctx:
25 25 try:
26 26 data = ctx[f].data()
27 27 except IOError, err:
28 28 if err.errno != errno.ENOENT:
29 29 raise
30 30 # handle missing subrepo spec files as removed
31 31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
32 32 return
33 33 p.parse(f, data, sections, remap, read)
34 34 else:
35 35 raise util.Abort(_("subrepo spec file %s not found") % f)
36 36
37 37 if '.hgsub' in ctx:
38 38 read('.hgsub')
39 39
40 40 for path, src in ui.configitems('subpaths'):
41 41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
42 42
43 43 rev = {}
44 44 if '.hgsubstate' in ctx:
45 45 try:
46 46 for l in ctx['.hgsubstate'].data().splitlines():
47 47 revision, path = l.split(" ", 1)
48 48 rev[path] = revision
49 49 except IOError, err:
50 50 if err.errno != errno.ENOENT:
51 51 raise
52 52
53 53 state = {}
54 54 for path, src in p[''].items():
55 55 kind = 'hg'
56 56 if src.startswith('['):
57 57 if ']' not in src:
58 58 raise util.Abort(_('missing ] in subrepo source'))
59 59 kind, src = src.split(']', 1)
60 60 kind = kind[1:]
61 61
62 62 for pattern, repl in p.items('subpaths'):
63 63 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
64 64 # does a string decode.
65 65 repl = repl.encode('string-escape')
66 66 # However, we still want to allow back references to go
67 67 # through unharmed, so we turn r'\\1' into r'\1'. Again,
68 68 # extra escapes are needed because re.sub string decodes.
69 69 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
70 70 try:
71 71 src = re.sub(pattern, repl, src, 1)
72 72 except re.error, e:
73 73 raise util.Abort(_("bad subrepository pattern in %s: %s")
74 74 % (p.source('subpaths', pattern), e))
75 75
76 76 state[path] = (src.strip(), rev.get(path, ''), kind)
77 77
78 78 return state
79 79
80 80 def writestate(repo, state):
81 81 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
82 82 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
83 83 repo.wwrite('.hgsubstate', ''.join(lines), '')
84 84
85 85 def submerge(repo, wctx, mctx, actx, overwrite):
86 86 """delegated from merge.applyupdates: merging of .hgsubstate file
87 87 in working context, merging context and ancestor context"""
88 88 if mctx == actx: # backwards?
89 89 actx = wctx.p1()
90 90 s1 = wctx.substate
91 91 s2 = mctx.substate
92 92 sa = actx.substate
93 93 sm = {}
94 94
95 95 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
96 96
97 97 def debug(s, msg, r=""):
98 98 if r:
99 99 r = "%s:%s:%s" % r
100 100 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
101 101
102 102 for s, l in s1.items():
103 103 a = sa.get(s, nullstate)
104 104 ld = l # local state with possible dirty flag for compares
105 105 if wctx.sub(s).dirty():
106 106 ld = (l[0], l[1] + "+")
107 107 if wctx == actx: # overwrite
108 108 a = ld
109 109
110 110 if s in s2:
111 111 r = s2[s]
112 112 if ld == r or r == a: # no change or local is newer
113 113 sm[s] = l
114 114 continue
115 115 elif ld == a: # other side changed
116 116 debug(s, "other changed, get", r)
117 117 wctx.sub(s).get(r, overwrite)
118 118 sm[s] = r
119 119 elif ld[0] != r[0]: # sources differ
120 120 if repo.ui.promptchoice(
121 121 _(' subrepository sources for %s differ\n'
122 122 'use (l)ocal source (%s) or (r)emote source (%s)?')
123 123 % (s, l[0], r[0]),
124 124 (_('&Local'), _('&Remote')), 0):
125 125 debug(s, "prompt changed, get", r)
126 126 wctx.sub(s).get(r, overwrite)
127 127 sm[s] = r
128 128 elif ld[1] == a[1]: # local side is unchanged
129 129 debug(s, "other side changed, get", r)
130 130 wctx.sub(s).get(r, overwrite)
131 131 sm[s] = r
132 132 else:
133 133 debug(s, "both sides changed, merge with", r)
134 134 wctx.sub(s).merge(r)
135 135 sm[s] = l
136 136 elif ld == a: # remote removed, local unchanged
137 137 debug(s, "remote removed, remove")
138 138 wctx.sub(s).remove()
139 139 elif a == nullstate: # not present in remote or ancestor
140 140 debug(s, "local added, keep")
141 141 sm[s] = l
142 142 continue
143 143 else:
144 144 if repo.ui.promptchoice(
145 145 _(' local changed subrepository %s which remote removed\n'
146 146 'use (c)hanged version or (d)elete?') % s,
147 147 (_('&Changed'), _('&Delete')), 0):
148 148 debug(s, "prompt remove")
149 149 wctx.sub(s).remove()
150 150
151 151 for s, r in sorted(s2.items()):
152 152 if s in s1:
153 153 continue
154 154 elif s not in sa:
155 155 debug(s, "remote added, get", r)
156 156 mctx.sub(s).get(r)
157 157 sm[s] = r
158 158 elif r != sa[s]:
159 159 if repo.ui.promptchoice(
160 160 _(' remote changed subrepository %s which local removed\n'
161 161 'use (c)hanged version or (d)elete?') % s,
162 162 (_('&Changed'), _('&Delete')), 0) == 0:
163 163 debug(s, "prompt recreate", r)
164 164 wctx.sub(s).get(r)
165 165 sm[s] = r
166 166
167 167 # record merged .hgsubstate
168 168 writestate(repo, sm)
169 169
170 170 def _updateprompt(ui, sub, dirty, local, remote):
171 171 if dirty:
172 172 msg = (_(' subrepository sources for %s differ\n'
173 173 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
174 174 % (subrelpath(sub), local, remote))
175 175 else:
176 176 msg = (_(' subrepository sources for %s differ (in checked out version)\n'
177 177 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
178 178 % (subrelpath(sub), local, remote))
179 179 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
180 180
181 181 def reporelpath(repo):
182 182 """return path to this (sub)repo as seen from outermost repo"""
183 183 parent = repo
184 184 while hasattr(parent, '_subparent'):
185 185 parent = parent._subparent
186 186 return repo.root[len(parent.root)+1:]
187 187
188 188 def subrelpath(sub):
189 189 """return path to this subrepo as seen from outermost repo"""
190 190 if hasattr(sub, '_relpath'):
191 191 return sub._relpath
192 192 if not hasattr(sub, '_repo'):
193 193 return sub._path
194 194 return reporelpath(sub._repo)
195 195
196 196 def _abssource(repo, push=False, abort=True):
197 197 """return pull/push path of repo - either based on parent repo .hgsub info
198 198 or on the top repo config. Abort or return None if no source found."""
199 199 if hasattr(repo, '_subparent'):
200 200 source = util.url(repo._subsource)
201 if source.isabs():
202 return str(source)
201 203 source.path = posixpath.normpath(source.path)
202 if posixpath.isabs(source.path) or source.scheme:
203 return str(source)
204 204 parent = _abssource(repo._subparent, push, abort=False)
205 205 if parent:
206 206 parent = util.url(parent)
207 207 parent.path = posixpath.join(parent.path, source.path)
208 208 parent.path = posixpath.normpath(parent.path)
209 209 return str(parent)
210 210 else: # recursion reached top repo
211 211 if hasattr(repo, '_subtoppath'):
212 212 return repo._subtoppath
213 213 if push and repo.ui.config('paths', 'default-push'):
214 214 return repo.ui.config('paths', 'default-push')
215 215 if repo.ui.config('paths', 'default'):
216 216 return repo.ui.config('paths', 'default')
217 217 if abort:
218 218 raise util.Abort(_("default path for subrepository %s not found") %
219 219 reporelpath(repo))
220 220
221 221 def itersubrepos(ctx1, ctx2):
222 222 """find subrepos in ctx1 or ctx2"""
223 223 # Create a (subpath, ctx) mapping where we prefer subpaths from
224 224 # ctx1. The subpaths from ctx2 are important when the .hgsub file
225 225 # has been modified (in ctx2) but not yet committed (in ctx1).
226 226 subpaths = dict.fromkeys(ctx2.substate, ctx2)
227 227 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
228 228 for subpath, ctx in sorted(subpaths.iteritems()):
229 229 yield subpath, ctx.sub(subpath)
230 230
231 231 def subrepo(ctx, path):
232 232 """return instance of the right subrepo class for subrepo in path"""
233 233 # subrepo inherently violates our import layering rules
234 234 # because it wants to make repo objects from deep inside the stack
235 235 # so we manually delay the circular imports to not break
236 236 # scripts that don't use our demand-loading
237 237 global hg
238 238 import hg as h
239 239 hg = h
240 240
241 241 scmutil.pathauditor(ctx._repo.root)(path)
242 242 state = ctx.substate.get(path, nullstate)
243 243 if state[2] not in types:
244 244 raise util.Abort(_('unknown subrepo type %s') % state[2])
245 245 return types[state[2]](ctx, path, state[:2])
246 246
247 247 # subrepo classes need to implement the following abstract class:
248 248
249 249 class abstractsubrepo(object):
250 250
251 251 def dirty(self, ignoreupdate=False):
252 252 """returns true if the dirstate of the subrepo is dirty or does not
253 253 match current stored state. If ignoreupdate is true, only check
254 254 whether the subrepo has uncommitted changes in its dirstate.
255 255 """
256 256 raise NotImplementedError
257 257
258 258 def checknested(self, path):
259 259 """check if path is a subrepository within this repository"""
260 260 return False
261 261
262 262 def commit(self, text, user, date):
263 263 """commit the current changes to the subrepo with the given
264 264 log message. Use given user and date if possible. Return the
265 265 new state of the subrepo.
266 266 """
267 267 raise NotImplementedError
268 268
269 269 def remove(self):
270 270 """remove the subrepo
271 271
272 272 (should verify the dirstate is not dirty first)
273 273 """
274 274 raise NotImplementedError
275 275
276 276 def get(self, state, overwrite=False):
277 277 """run whatever commands are needed to put the subrepo into
278 278 this state
279 279 """
280 280 raise NotImplementedError
281 281
282 282 def merge(self, state):
283 283 """merge currently-saved state with the new state."""
284 284 raise NotImplementedError
285 285
286 286 def push(self, force):
287 287 """perform whatever action is analogous to 'hg push'
288 288
289 289 This may be a no-op on some systems.
290 290 """
291 291 raise NotImplementedError
292 292
293 293 def add(self, ui, match, dryrun, prefix):
294 294 return []
295 295
296 296 def status(self, rev2, **opts):
297 297 return [], [], [], [], [], [], []
298 298
299 299 def diff(self, diffopts, node2, match, prefix, **opts):
300 300 pass
301 301
302 302 def outgoing(self, ui, dest, opts):
303 303 return 1
304 304
305 305 def incoming(self, ui, source, opts):
306 306 return 1
307 307
308 308 def files(self):
309 309 """return filename iterator"""
310 310 raise NotImplementedError
311 311
312 312 def filedata(self, name):
313 313 """return file data"""
314 314 raise NotImplementedError
315 315
316 316 def fileflags(self, name):
317 317 """return file flags"""
318 318 return ''
319 319
320 320 def archive(self, ui, archiver, prefix):
321 321 files = self.files()
322 322 total = len(files)
323 323 relpath = subrelpath(self)
324 324 ui.progress(_('archiving (%s)') % relpath, 0,
325 325 unit=_('files'), total=total)
326 326 for i, name in enumerate(files):
327 327 flags = self.fileflags(name)
328 328 mode = 'x' in flags and 0755 or 0644
329 329 symlink = 'l' in flags
330 330 archiver.addfile(os.path.join(prefix, self._path, name),
331 331 mode, symlink, self.filedata(name))
332 332 ui.progress(_('archiving (%s)') % relpath, i + 1,
333 333 unit=_('files'), total=total)
334 334 ui.progress(_('archiving (%s)') % relpath, None)
335 335
336 336
337 337 class hgsubrepo(abstractsubrepo):
338 338 def __init__(self, ctx, path, state):
339 339 self._path = path
340 340 self._state = state
341 341 r = ctx._repo
342 342 root = r.wjoin(path)
343 343 create = False
344 344 if not os.path.exists(os.path.join(root, '.hg')):
345 345 create = True
346 346 util.makedirs(root)
347 347 self._repo = hg.repository(r.ui, root, create=create)
348 348 self._initrepo(r, state[0], create)
349 349
350 350 def _initrepo(self, parentrepo, source, create):
351 351 self._repo._subparent = parentrepo
352 352 self._repo._subsource = source
353 353
354 354 if create:
355 355 fp = self._repo.opener("hgrc", "w", text=True)
356 356 fp.write('[paths]\n')
357 357
358 358 def addpathconfig(key, value):
359 359 if value:
360 360 fp.write('%s = %s\n' % (key, value))
361 361 self._repo.ui.setconfig('paths', key, value)
362 362
363 363 defpath = _abssource(self._repo, abort=False)
364 364 defpushpath = _abssource(self._repo, True, abort=False)
365 365 addpathconfig('default', defpath)
366 366 if defpath != defpushpath:
367 367 addpathconfig('default-push', defpushpath)
368 368 fp.close()
369 369
370 370 def add(self, ui, match, dryrun, prefix):
371 371 return cmdutil.add(ui, self._repo, match, dryrun, True,
372 372 os.path.join(prefix, self._path))
373 373
374 374 def status(self, rev2, **opts):
375 375 try:
376 376 rev1 = self._state[1]
377 377 ctx1 = self._repo[rev1]
378 378 ctx2 = self._repo[rev2]
379 379 return self._repo.status(ctx1, ctx2, **opts)
380 380 except error.RepoLookupError, inst:
381 381 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
382 382 % (inst, subrelpath(self)))
383 383 return [], [], [], [], [], [], []
384 384
385 385 def diff(self, diffopts, node2, match, prefix, **opts):
386 386 try:
387 387 node1 = node.bin(self._state[1])
388 388 # We currently expect node2 to come from substate and be
389 389 # in hex format
390 390 if node2 is not None:
391 391 node2 = node.bin(node2)
392 392 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
393 393 node1, node2, match,
394 394 prefix=os.path.join(prefix, self._path),
395 395 listsubrepos=True, **opts)
396 396 except error.RepoLookupError, inst:
397 397 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
398 398 % (inst, subrelpath(self)))
399 399
400 400 def archive(self, ui, archiver, prefix):
401 401 abstractsubrepo.archive(self, ui, archiver, prefix)
402 402
403 403 rev = self._state[1]
404 404 ctx = self._repo[rev]
405 405 for subpath in ctx.substate:
406 406 s = subrepo(ctx, subpath)
407 407 s.archive(ui, archiver, os.path.join(prefix, self._path))
408 408
409 409 def dirty(self, ignoreupdate=False):
410 410 r = self._state[1]
411 411 if r == '' and not ignoreupdate: # no state recorded
412 412 return True
413 413 w = self._repo[None]
414 414 if r != w.p1().hex() and not ignoreupdate:
415 415 # different version checked out
416 416 return True
417 417 return w.dirty() # working directory changed
418 418
419 419 def checknested(self, path):
420 420 return self._repo._checknested(self._repo.wjoin(path))
421 421
422 422 def commit(self, text, user, date):
423 423 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
424 424 n = self._repo.commit(text, user, date)
425 425 if not n:
426 426 return self._repo['.'].hex() # different version checked out
427 427 return node.hex(n)
428 428
429 429 def remove(self):
430 430 # we can't fully delete the repository as it may contain
431 431 # local-only history
432 432 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
433 433 hg.clean(self._repo, node.nullid, False)
434 434
435 435 def _get(self, state):
436 436 source, revision, kind = state
437 437 if revision not in self._repo:
438 438 self._repo._subsource = source
439 439 srcurl = _abssource(self._repo)
440 440 other = hg.peer(self._repo.ui, {}, srcurl)
441 441 if len(self._repo) == 0:
442 442 self._repo.ui.status(_('cloning subrepo %s from %s\n')
443 443 % (subrelpath(self), srcurl))
444 444 parentrepo = self._repo._subparent
445 445 shutil.rmtree(self._repo.root)
446 446 other, self._repo = hg.clone(self._repo._subparent.ui, {}, other,
447 447 self._repo.root, update=False)
448 448 self._initrepo(parentrepo, source, create=True)
449 449 else:
450 450 self._repo.ui.status(_('pulling subrepo %s from %s\n')
451 451 % (subrelpath(self), srcurl))
452 452 self._repo.pull(other)
453 453 bookmarks.updatefromremote(self._repo.ui, self._repo, other)
454 454
455 455 def get(self, state, overwrite=False):
456 456 self._get(state)
457 457 source, revision, kind = state
458 458 self._repo.ui.debug("getting subrepo %s\n" % self._path)
459 459 hg.clean(self._repo, revision, False)
460 460
461 461 def merge(self, state):
462 462 self._get(state)
463 463 cur = self._repo['.']
464 464 dst = self._repo[state[1]]
465 465 anc = dst.ancestor(cur)
466 466
467 467 def mergefunc():
468 468 if anc == cur:
469 469 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
470 470 hg.update(self._repo, state[1])
471 471 elif anc == dst:
472 472 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
473 473 else:
474 474 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
475 475 hg.merge(self._repo, state[1], remind=False)
476 476
477 477 wctx = self._repo[None]
478 478 if self.dirty():
479 479 if anc != dst:
480 480 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
481 481 mergefunc()
482 482 else:
483 483 mergefunc()
484 484 else:
485 485 mergefunc()
486 486
487 487 def push(self, force):
488 488 # push subrepos depth-first for coherent ordering
489 489 c = self._repo['']
490 490 subs = c.substate # only repos that are committed
491 491 for s in sorted(subs):
492 492 if not c.sub(s).push(force):
493 493 return False
494 494
495 495 dsturl = _abssource(self._repo, True)
496 496 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
497 497 (subrelpath(self), dsturl))
498 498 other = hg.peer(self._repo.ui, {}, dsturl)
499 499 return self._repo.push(other, force)
500 500
501 501 def outgoing(self, ui, dest, opts):
502 502 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
503 503
504 504 def incoming(self, ui, source, opts):
505 505 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
506 506
507 507 def files(self):
508 508 rev = self._state[1]
509 509 ctx = self._repo[rev]
510 510 return ctx.manifest()
511 511
512 512 def filedata(self, name):
513 513 rev = self._state[1]
514 514 return self._repo[rev][name].data()
515 515
516 516 def fileflags(self, name):
517 517 rev = self._state[1]
518 518 ctx = self._repo[rev]
519 519 return ctx.flags(name)
520 520
521 521
522 522 class svnsubrepo(abstractsubrepo):
523 523 def __init__(self, ctx, path, state):
524 524 self._path = path
525 525 self._state = state
526 526 self._ctx = ctx
527 527 self._ui = ctx._repo.ui
528 528
529 529 def _svncommand(self, commands, filename='', failok=False):
530 530 cmd = ['svn']
531 531 extrakw = {}
532 532 if not self._ui.interactive():
533 533 # Making stdin be a pipe should prevent svn from behaving
534 534 # interactively even if we can't pass --non-interactive.
535 535 extrakw['stdin'] = subprocess.PIPE
536 536 # Starting in svn 1.5 --non-interactive is a global flag
537 537 # instead of being per-command, but we need to support 1.4 so
538 538 # we have to be intelligent about what commands take
539 539 # --non-interactive.
540 540 if commands[0] in ('update', 'checkout', 'commit'):
541 541 cmd.append('--non-interactive')
542 542 cmd.extend(commands)
543 543 if filename is not None:
544 544 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
545 545 cmd.append(path)
546 546 env = dict(os.environ)
547 547 # Avoid localized output, preserve current locale for everything else.
548 548 env['LC_MESSAGES'] = 'C'
549 549 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
550 550 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
551 551 universal_newlines=True, env=env, **extrakw)
552 552 stdout, stderr = p.communicate()
553 553 stderr = stderr.strip()
554 554 if not failok:
555 555 if p.returncode:
556 556 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
557 557 if stderr:
558 558 self._ui.warn(stderr + '\n')
559 559 return stdout, stderr
560 560
561 561 @propertycache
562 562 def _svnversion(self):
563 563 output, err = self._svncommand(['--version'], filename=None)
564 564 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
565 565 if not m:
566 566 raise util.Abort(_('cannot retrieve svn tool version'))
567 567 return (int(m.group(1)), int(m.group(2)))
568 568
569 569 def _wcrevs(self):
570 570 # Get the working directory revision as well as the last
571 571 # commit revision so we can compare the subrepo state with
572 572 # both. We used to store the working directory one.
573 573 output, err = self._svncommand(['info', '--xml'])
574 574 doc = xml.dom.minidom.parseString(output)
575 575 entries = doc.getElementsByTagName('entry')
576 576 lastrev, rev = '0', '0'
577 577 if entries:
578 578 rev = str(entries[0].getAttribute('revision')) or '0'
579 579 commits = entries[0].getElementsByTagName('commit')
580 580 if commits:
581 581 lastrev = str(commits[0].getAttribute('revision')) or '0'
582 582 return (lastrev, rev)
583 583
584 584 def _wcrev(self):
585 585 return self._wcrevs()[0]
586 586
587 587 def _wcchanged(self):
588 588 """Return (changes, extchanges) where changes is True
589 589 if the working directory was changed, and extchanges is
590 590 True if any of these changes concern an external entry.
591 591 """
592 592 output, err = self._svncommand(['status', '--xml'])
593 593 externals, changes = [], []
594 594 doc = xml.dom.minidom.parseString(output)
595 595 for e in doc.getElementsByTagName('entry'):
596 596 s = e.getElementsByTagName('wc-status')
597 597 if not s:
598 598 continue
599 599 item = s[0].getAttribute('item')
600 600 props = s[0].getAttribute('props')
601 601 path = e.getAttribute('path')
602 602 if item == 'external':
603 603 externals.append(path)
604 604 if (item not in ('', 'normal', 'unversioned', 'external')
605 605 or props not in ('', 'none')):
606 606 changes.append(path)
607 607 for path in changes:
608 608 for ext in externals:
609 609 if path == ext or path.startswith(ext + os.sep):
610 610 return True, True
611 611 return bool(changes), False
612 612
613 613 def dirty(self, ignoreupdate=False):
614 614 if not self._wcchanged()[0]:
615 615 if self._state[1] in self._wcrevs() or ignoreupdate:
616 616 return False
617 617 return True
618 618
619 619 def commit(self, text, user, date):
620 620 # user and date are out of our hands since svn is centralized
621 621 changed, extchanged = self._wcchanged()
622 622 if not changed:
623 623 return self._wcrev()
624 624 if extchanged:
625 625 # Do not try to commit externals
626 626 raise util.Abort(_('cannot commit svn externals'))
627 627 commitinfo, err = self._svncommand(['commit', '-m', text])
628 628 self._ui.status(commitinfo)
629 629 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
630 630 if not newrev:
631 631 raise util.Abort(commitinfo.splitlines()[-1])
632 632 newrev = newrev.groups()[0]
633 633 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
634 634 return newrev
635 635
636 636 def remove(self):
637 637 if self.dirty():
638 638 self._ui.warn(_('not removing repo %s because '
639 639 'it has changes.\n' % self._path))
640 640 return
641 641 self._ui.note(_('removing subrepo %s\n') % self._path)
642 642
643 643 def onerror(function, path, excinfo):
644 644 if function is not os.remove:
645 645 raise
646 646 # read-only files cannot be unlinked under Windows
647 647 s = os.stat(path)
648 648 if (s.st_mode & stat.S_IWRITE) != 0:
649 649 raise
650 650 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
651 651 os.remove(path)
652 652
653 653 path = self._ctx._repo.wjoin(self._path)
654 654 shutil.rmtree(path, onerror=onerror)
655 655 try:
656 656 os.removedirs(os.path.dirname(path))
657 657 except OSError:
658 658 pass
659 659
660 660 def get(self, state, overwrite=False):
661 661 if overwrite:
662 662 self._svncommand(['revert', '--recursive'])
663 663 args = ['checkout']
664 664 if self._svnversion >= (1, 5):
665 665 args.append('--force')
666 666 args.extend([state[0], '--revision', state[1]])
667 667 status, err = self._svncommand(args, failok=True)
668 668 if not re.search('Checked out revision [0-9]+.', status):
669 669 if ('is already a working copy for a different URL' in err
670 670 and (self._wcchanged() == (False, False))):
671 671 # obstructed but clean working copy, so just blow it away.
672 672 self.remove()
673 673 self.get(state, overwrite=False)
674 674 return
675 675 raise util.Abort((status or err).splitlines()[-1])
676 676 self._ui.status(status)
677 677
678 678 def merge(self, state):
679 679 old = self._state[1]
680 680 new = state[1]
681 681 if new != self._wcrev():
682 682 dirty = old == self._wcrev() or self._wcchanged()[0]
683 683 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
684 684 self.get(state, False)
685 685
686 686 def push(self, force):
687 687 # push is a no-op for SVN
688 688 return True
689 689
690 690 def files(self):
691 691 output = self._svncommand(['list'])
692 692 # This works because svn forbids \n in filenames.
693 693 return output.splitlines()
694 694
695 695 def filedata(self, name):
696 696 return self._svncommand(['cat'], name)
697 697
698 698
699 699 class gitsubrepo(abstractsubrepo):
700 700 def __init__(self, ctx, path, state):
701 701 # TODO add git version check.
702 702 self._state = state
703 703 self._ctx = ctx
704 704 self._path = path
705 705 self._relpath = os.path.join(reporelpath(ctx._repo), path)
706 706 self._abspath = ctx._repo.wjoin(path)
707 707 self._subparent = ctx._repo
708 708 self._ui = ctx._repo.ui
709 709
710 710 def _gitcommand(self, commands, env=None, stream=False):
711 711 return self._gitdir(commands, env=env, stream=stream)[0]
712 712
713 713 def _gitdir(self, commands, env=None, stream=False):
714 714 return self._gitnodir(commands, env=env, stream=stream,
715 715 cwd=self._abspath)
716 716
717 717 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
718 718 """Calls the git command
719 719
720 720 The methods tries to call the git command. versions previor to 1.6.0
721 721 are not supported and very probably fail.
722 722 """
723 723 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
724 724 # unless ui.quiet is set, print git's stderr,
725 725 # which is mostly progress and useful info
726 726 errpipe = None
727 727 if self._ui.quiet:
728 728 errpipe = open(os.devnull, 'w')
729 729 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
730 730 close_fds=util.closefds,
731 731 stdout=subprocess.PIPE, stderr=errpipe)
732 732 if stream:
733 733 return p.stdout, None
734 734
735 735 retdata = p.stdout.read().strip()
736 736 # wait for the child to exit to avoid race condition.
737 737 p.wait()
738 738
739 739 if p.returncode != 0 and p.returncode != 1:
740 740 # there are certain error codes that are ok
741 741 command = commands[0]
742 742 if command in ('cat-file', 'symbolic-ref'):
743 743 return retdata, p.returncode
744 744 # for all others, abort
745 745 raise util.Abort('git %s error %d in %s' %
746 746 (command, p.returncode, self._relpath))
747 747
748 748 return retdata, p.returncode
749 749
750 750 def _gitmissing(self):
751 751 return not os.path.exists(os.path.join(self._abspath, '.git'))
752 752
753 753 def _gitstate(self):
754 754 return self._gitcommand(['rev-parse', 'HEAD'])
755 755
756 756 def _gitcurrentbranch(self):
757 757 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
758 758 if err:
759 759 current = None
760 760 return current
761 761
762 762 def _gitremote(self, remote):
763 763 out = self._gitcommand(['remote', 'show', '-n', remote])
764 764 line = out.split('\n')[1]
765 765 i = line.index('URL: ') + len('URL: ')
766 766 return line[i:]
767 767
768 768 def _githavelocally(self, revision):
769 769 out, code = self._gitdir(['cat-file', '-e', revision])
770 770 return code == 0
771 771
772 772 def _gitisancestor(self, r1, r2):
773 773 base = self._gitcommand(['merge-base', r1, r2])
774 774 return base == r1
775 775
776 776 def _gitisbare(self):
777 777 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
778 778
779 779 def _gitbranchmap(self):
780 780 '''returns 2 things:
781 781 a map from git branch to revision
782 782 a map from revision to branches'''
783 783 branch2rev = {}
784 784 rev2branch = {}
785 785
786 786 out = self._gitcommand(['for-each-ref', '--format',
787 787 '%(objectname) %(refname)'])
788 788 for line in out.split('\n'):
789 789 revision, ref = line.split(' ')
790 790 if (not ref.startswith('refs/heads/') and
791 791 not ref.startswith('refs/remotes/')):
792 792 continue
793 793 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
794 794 continue # ignore remote/HEAD redirects
795 795 branch2rev[ref] = revision
796 796 rev2branch.setdefault(revision, []).append(ref)
797 797 return branch2rev, rev2branch
798 798
799 799 def _gittracking(self, branches):
800 800 'return map of remote branch to local tracking branch'
801 801 # assumes no more than one local tracking branch for each remote
802 802 tracking = {}
803 803 for b in branches:
804 804 if b.startswith('refs/remotes/'):
805 805 continue
806 806 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
807 807 if remote:
808 808 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
809 809 tracking['refs/remotes/%s/%s' %
810 810 (remote, ref.split('/', 2)[2])] = b
811 811 return tracking
812 812
813 813 def _abssource(self, source):
814 814 if '://' not in source:
815 815 # recognize the scp syntax as an absolute source
816 816 colon = source.find(':')
817 817 if colon != -1 and '/' not in source[:colon]:
818 818 return source
819 819 self._subsource = source
820 820 return _abssource(self)
821 821
822 822 def _fetch(self, source, revision):
823 823 if self._gitmissing():
824 824 source = self._abssource(source)
825 825 self._ui.status(_('cloning subrepo %s from %s\n') %
826 826 (self._relpath, source))
827 827 self._gitnodir(['clone', source, self._abspath])
828 828 if self._githavelocally(revision):
829 829 return
830 830 self._ui.status(_('pulling subrepo %s from %s\n') %
831 831 (self._relpath, self._gitremote('origin')))
832 832 # try only origin: the originally cloned repo
833 833 self._gitcommand(['fetch'])
834 834 if not self._githavelocally(revision):
835 835 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
836 836 (revision, self._relpath))
837 837
838 838 def dirty(self, ignoreupdate=False):
839 839 if self._gitmissing():
840 840 return self._state[1] != ''
841 841 if self._gitisbare():
842 842 return True
843 843 if not ignoreupdate and self._state[1] != self._gitstate():
844 844 # different version checked out
845 845 return True
846 846 # check for staged changes or modified files; ignore untracked files
847 847 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
848 848 return code == 1
849 849
850 850 def get(self, state, overwrite=False):
851 851 source, revision, kind = state
852 852 if not revision:
853 853 self.remove()
854 854 return
855 855 self._fetch(source, revision)
856 856 # if the repo was set to be bare, unbare it
857 857 if self._gitisbare():
858 858 self._gitcommand(['config', 'core.bare', 'false'])
859 859 if self._gitstate() == revision:
860 860 self._gitcommand(['reset', '--hard', 'HEAD'])
861 861 return
862 862 elif self._gitstate() == revision:
863 863 if overwrite:
864 864 # first reset the index to unmark new files for commit, because
865 865 # reset --hard will otherwise throw away files added for commit,
866 866 # not just unmark them.
867 867 self._gitcommand(['reset', 'HEAD'])
868 868 self._gitcommand(['reset', '--hard', 'HEAD'])
869 869 return
870 870 branch2rev, rev2branch = self._gitbranchmap()
871 871
872 872 def checkout(args):
873 873 cmd = ['checkout']
874 874 if overwrite:
875 875 # first reset the index to unmark new files for commit, because
876 876 # the -f option will otherwise throw away files added for
877 877 # commit, not just unmark them.
878 878 self._gitcommand(['reset', 'HEAD'])
879 879 cmd.append('-f')
880 880 self._gitcommand(cmd + args)
881 881
882 882 def rawcheckout():
883 883 # no branch to checkout, check it out with no branch
884 884 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
885 885 self._relpath)
886 886 self._ui.warn(_('check out a git branch if you intend '
887 887 'to make changes\n'))
888 888 checkout(['-q', revision])
889 889
890 890 if revision not in rev2branch:
891 891 rawcheckout()
892 892 return
893 893 branches = rev2branch[revision]
894 894 firstlocalbranch = None
895 895 for b in branches:
896 896 if b == 'refs/heads/master':
897 897 # master trumps all other branches
898 898 checkout(['refs/heads/master'])
899 899 return
900 900 if not firstlocalbranch and not b.startswith('refs/remotes/'):
901 901 firstlocalbranch = b
902 902 if firstlocalbranch:
903 903 checkout([firstlocalbranch])
904 904 return
905 905
906 906 tracking = self._gittracking(branch2rev.keys())
907 907 # choose a remote branch already tracked if possible
908 908 remote = branches[0]
909 909 if remote not in tracking:
910 910 for b in branches:
911 911 if b in tracking:
912 912 remote = b
913 913 break
914 914
915 915 if remote not in tracking:
916 916 # create a new local tracking branch
917 917 local = remote.split('/', 2)[2]
918 918 checkout(['-b', local, remote])
919 919 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
920 920 # When updating to a tracked remote branch,
921 921 # if the local tracking branch is downstream of it,
922 922 # a normal `git pull` would have performed a "fast-forward merge"
923 923 # which is equivalent to updating the local branch to the remote.
924 924 # Since we are only looking at branching at update, we need to
925 925 # detect this situation and perform this action lazily.
926 926 if tracking[remote] != self._gitcurrentbranch():
927 927 checkout([tracking[remote]])
928 928 self._gitcommand(['merge', '--ff', remote])
929 929 else:
930 930 # a real merge would be required, just checkout the revision
931 931 rawcheckout()
932 932
933 933 def commit(self, text, user, date):
934 934 if self._gitmissing():
935 935 raise util.Abort(_("subrepo %s is missing") % self._relpath)
936 936 cmd = ['commit', '-a', '-m', text]
937 937 env = os.environ.copy()
938 938 if user:
939 939 cmd += ['--author', user]
940 940 if date:
941 941 # git's date parser silently ignores when seconds < 1e9
942 942 # convert to ISO8601
943 943 env['GIT_AUTHOR_DATE'] = util.datestr(date,
944 944 '%Y-%m-%dT%H:%M:%S %1%2')
945 945 self._gitcommand(cmd, env=env)
946 946 # make sure commit works otherwise HEAD might not exist under certain
947 947 # circumstances
948 948 return self._gitstate()
949 949
950 950 def merge(self, state):
951 951 source, revision, kind = state
952 952 self._fetch(source, revision)
953 953 base = self._gitcommand(['merge-base', revision, self._state[1]])
954 954 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
955 955
956 956 def mergefunc():
957 957 if base == revision:
958 958 self.get(state) # fast forward merge
959 959 elif base != self._state[1]:
960 960 self._gitcommand(['merge', '--no-commit', revision])
961 961
962 962 if self.dirty():
963 963 if self._gitstate() != revision:
964 964 dirty = self._gitstate() == self._state[1] or code != 0
965 965 if _updateprompt(self._ui, self, dirty,
966 966 self._state[1][:7], revision[:7]):
967 967 mergefunc()
968 968 else:
969 969 mergefunc()
970 970
971 971 def push(self, force):
972 972 if not self._state[1]:
973 973 return True
974 974 if self._gitmissing():
975 975 raise util.Abort(_("subrepo %s is missing") % self._relpath)
976 976 # if a branch in origin contains the revision, nothing to do
977 977 branch2rev, rev2branch = self._gitbranchmap()
978 978 if self._state[1] in rev2branch:
979 979 for b in rev2branch[self._state[1]]:
980 980 if b.startswith('refs/remotes/origin/'):
981 981 return True
982 982 for b, revision in branch2rev.iteritems():
983 983 if b.startswith('refs/remotes/origin/'):
984 984 if self._gitisancestor(self._state[1], revision):
985 985 return True
986 986 # otherwise, try to push the currently checked out branch
987 987 cmd = ['push']
988 988 if force:
989 989 cmd.append('--force')
990 990
991 991 current = self._gitcurrentbranch()
992 992 if current:
993 993 # determine if the current branch is even useful
994 994 if not self._gitisancestor(self._state[1], current):
995 995 self._ui.warn(_('unrelated git branch checked out '
996 996 'in subrepo %s\n') % self._relpath)
997 997 return False
998 998 self._ui.status(_('pushing branch %s of subrepo %s\n') %
999 999 (current.split('/', 2)[2], self._relpath))
1000 1000 self._gitcommand(cmd + ['origin', current])
1001 1001 return True
1002 1002 else:
1003 1003 self._ui.warn(_('no branch checked out in subrepo %s\n'
1004 1004 'cannot push revision %s') %
1005 1005 (self._relpath, self._state[1]))
1006 1006 return False
1007 1007
1008 1008 def remove(self):
1009 1009 if self._gitmissing():
1010 1010 return
1011 1011 if self.dirty():
1012 1012 self._ui.warn(_('not removing repo %s because '
1013 1013 'it has changes.\n') % self._relpath)
1014 1014 return
1015 1015 # we can't fully delete the repository as it may contain
1016 1016 # local-only history
1017 1017 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1018 1018 self._gitcommand(['config', 'core.bare', 'true'])
1019 1019 for f in os.listdir(self._abspath):
1020 1020 if f == '.git':
1021 1021 continue
1022 1022 path = os.path.join(self._abspath, f)
1023 1023 if os.path.isdir(path) and not os.path.islink(path):
1024 1024 shutil.rmtree(path)
1025 1025 else:
1026 1026 os.remove(path)
1027 1027
1028 1028 def archive(self, ui, archiver, prefix):
1029 1029 source, revision = self._state
1030 1030 if not revision:
1031 1031 return
1032 1032 self._fetch(source, revision)
1033 1033
1034 1034 # Parse git's native archive command.
1035 1035 # This should be much faster than manually traversing the trees
1036 1036 # and objects with many subprocess calls.
1037 1037 tarstream = self._gitcommand(['archive', revision], stream=True)
1038 1038 tar = tarfile.open(fileobj=tarstream, mode='r|')
1039 1039 relpath = subrelpath(self)
1040 1040 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1041 1041 for i, info in enumerate(tar):
1042 1042 if info.isdir():
1043 1043 continue
1044 1044 if info.issym():
1045 1045 data = info.linkname
1046 1046 else:
1047 1047 data = tar.extractfile(info).read()
1048 1048 archiver.addfile(os.path.join(prefix, self._path, info.name),
1049 1049 info.mode, info.issym(), data)
1050 1050 ui.progress(_('archiving (%s)') % relpath, i + 1,
1051 1051 unit=_('files'))
1052 1052 ui.progress(_('archiving (%s)') % relpath, None)
1053 1053
1054 1054
1055 1055 def status(self, rev2, **opts):
1056 1056 rev1 = self._state[1]
1057 1057 if self._gitmissing() or not rev1:
1058 1058 # if the repo is missing, return no results
1059 1059 return [], [], [], [], [], [], []
1060 1060 modified, added, removed = [], [], []
1061 1061 if rev2:
1062 1062 command = ['diff-tree', rev1, rev2]
1063 1063 else:
1064 1064 command = ['diff-index', rev1]
1065 1065 out = self._gitcommand(command)
1066 1066 for line in out.split('\n'):
1067 1067 tab = line.find('\t')
1068 1068 if tab == -1:
1069 1069 continue
1070 1070 status, f = line[tab - 1], line[tab + 1:]
1071 1071 if status == 'M':
1072 1072 modified.append(f)
1073 1073 elif status == 'A':
1074 1074 added.append(f)
1075 1075 elif status == 'D':
1076 1076 removed.append(f)
1077 1077
1078 1078 deleted = unknown = ignored = clean = []
1079 1079 return modified, added, removed, deleted, unknown, ignored, clean
1080 1080
1081 1081 types = {
1082 1082 'hg': hgsubrepo,
1083 1083 'svn': svnsubrepo,
1084 1084 'git': gitsubrepo,
1085 1085 }
@@ -1,1601 +1,1612 b''
1 1 # util.py - Mercurial utility functions and platform specfic implementations
2 2 #
3 3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 """Mercurial utility functions and platform specfic implementations.
11 11
12 12 This contains helper routines that are independent of the SCM core and
13 13 hide platform-specific details from the core.
14 14 """
15 15
16 16 from i18n import _
17 17 import error, osutil, encoding
18 18 import errno, re, shutil, sys, tempfile, traceback
19 19 import os, time, calendar, textwrap, unicodedata, signal
20 20 import imp, socket, urllib
21 21
22 22 # Python compatibility
23 23
24 24 def sha1(s):
25 25 return _fastsha1(s)
26 26
27 27 def _fastsha1(s):
28 28 # This function will import sha1 from hashlib or sha (whichever is
29 29 # available) and overwrite itself with it on the first call.
30 30 # Subsequent calls will go directly to the imported function.
31 31 if sys.version_info >= (2, 5):
32 32 from hashlib import sha1 as _sha1
33 33 else:
34 34 from sha import sha as _sha1
35 35 global _fastsha1, sha1
36 36 _fastsha1 = sha1 = _sha1
37 37 return _sha1(s)
38 38
39 39 import __builtin__
40 40
41 41 if sys.version_info[0] < 3:
42 42 def fakebuffer(sliceable, offset=0):
43 43 return sliceable[offset:]
44 44 else:
45 45 def fakebuffer(sliceable, offset=0):
46 46 return memoryview(sliceable)[offset:]
47 47 try:
48 48 buffer
49 49 except NameError:
50 50 __builtin__.buffer = fakebuffer
51 51
52 52 import subprocess
53 53 closefds = os.name == 'posix'
54 54
55 55 def popen2(cmd, env=None, newlines=False):
56 56 # Setting bufsize to -1 lets the system decide the buffer size.
57 57 # The default for bufsize is 0, meaning unbuffered. This leads to
58 58 # poor performance on Mac OS X: http://bugs.python.org/issue4194
59 59 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
60 60 close_fds=closefds,
61 61 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
62 62 universal_newlines=newlines,
63 63 env=env)
64 64 return p.stdin, p.stdout
65 65
66 66 def popen3(cmd, env=None, newlines=False):
67 67 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
68 68 close_fds=closefds,
69 69 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
70 70 stderr=subprocess.PIPE,
71 71 universal_newlines=newlines,
72 72 env=env)
73 73 return p.stdin, p.stdout, p.stderr
74 74
75 75 def version():
76 76 """Return version information if available."""
77 77 try:
78 78 import __version__
79 79 return __version__.version
80 80 except ImportError:
81 81 return 'unknown'
82 82
83 83 # used by parsedate
84 84 defaultdateformats = (
85 85 '%Y-%m-%d %H:%M:%S',
86 86 '%Y-%m-%d %I:%M:%S%p',
87 87 '%Y-%m-%d %H:%M',
88 88 '%Y-%m-%d %I:%M%p',
89 89 '%Y-%m-%d',
90 90 '%m-%d',
91 91 '%m/%d',
92 92 '%m/%d/%y',
93 93 '%m/%d/%Y',
94 94 '%a %b %d %H:%M:%S %Y',
95 95 '%a %b %d %I:%M:%S%p %Y',
96 96 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
97 97 '%b %d %H:%M:%S %Y',
98 98 '%b %d %I:%M:%S%p %Y',
99 99 '%b %d %H:%M:%S',
100 100 '%b %d %I:%M:%S%p',
101 101 '%b %d %H:%M',
102 102 '%b %d %I:%M%p',
103 103 '%b %d %Y',
104 104 '%b %d',
105 105 '%H:%M:%S',
106 106 '%I:%M:%S%p',
107 107 '%H:%M',
108 108 '%I:%M%p',
109 109 )
110 110
111 111 extendeddateformats = defaultdateformats + (
112 112 "%Y",
113 113 "%Y-%m",
114 114 "%b",
115 115 "%b %Y",
116 116 )
117 117
118 118 def cachefunc(func):
119 119 '''cache the result of function calls'''
120 120 # XXX doesn't handle keywords args
121 121 cache = {}
122 122 if func.func_code.co_argcount == 1:
123 123 # we gain a small amount of time because
124 124 # we don't need to pack/unpack the list
125 125 def f(arg):
126 126 if arg not in cache:
127 127 cache[arg] = func(arg)
128 128 return cache[arg]
129 129 else:
130 130 def f(*args):
131 131 if args not in cache:
132 132 cache[args] = func(*args)
133 133 return cache[args]
134 134
135 135 return f
136 136
137 137 def lrucachefunc(func):
138 138 '''cache most recent results of function calls'''
139 139 cache = {}
140 140 order = []
141 141 if func.func_code.co_argcount == 1:
142 142 def f(arg):
143 143 if arg not in cache:
144 144 if len(cache) > 20:
145 145 del cache[order.pop(0)]
146 146 cache[arg] = func(arg)
147 147 else:
148 148 order.remove(arg)
149 149 order.append(arg)
150 150 return cache[arg]
151 151 else:
152 152 def f(*args):
153 153 if args not in cache:
154 154 if len(cache) > 20:
155 155 del cache[order.pop(0)]
156 156 cache[args] = func(*args)
157 157 else:
158 158 order.remove(args)
159 159 order.append(args)
160 160 return cache[args]
161 161
162 162 return f
163 163
164 164 class propertycache(object):
165 165 def __init__(self, func):
166 166 self.func = func
167 167 self.name = func.__name__
168 168 def __get__(self, obj, type=None):
169 169 result = self.func(obj)
170 170 setattr(obj, self.name, result)
171 171 return result
172 172
173 173 def pipefilter(s, cmd):
174 174 '''filter string S through command CMD, returning its output'''
175 175 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
176 176 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
177 177 pout, perr = p.communicate(s)
178 178 return pout
179 179
180 180 def tempfilter(s, cmd):
181 181 '''filter string S through a pair of temporary files with CMD.
182 182 CMD is used as a template to create the real command to be run,
183 183 with the strings INFILE and OUTFILE replaced by the real names of
184 184 the temporary files generated.'''
185 185 inname, outname = None, None
186 186 try:
187 187 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
188 188 fp = os.fdopen(infd, 'wb')
189 189 fp.write(s)
190 190 fp.close()
191 191 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
192 192 os.close(outfd)
193 193 cmd = cmd.replace('INFILE', inname)
194 194 cmd = cmd.replace('OUTFILE', outname)
195 195 code = os.system(cmd)
196 196 if sys.platform == 'OpenVMS' and code & 1:
197 197 code = 0
198 198 if code:
199 199 raise Abort(_("command '%s' failed: %s") %
200 200 (cmd, explainexit(code)))
201 201 fp = open(outname, 'rb')
202 202 r = fp.read()
203 203 fp.close()
204 204 return r
205 205 finally:
206 206 try:
207 207 if inname:
208 208 os.unlink(inname)
209 209 except OSError:
210 210 pass
211 211 try:
212 212 if outname:
213 213 os.unlink(outname)
214 214 except OSError:
215 215 pass
216 216
217 217 filtertable = {
218 218 'tempfile:': tempfilter,
219 219 'pipe:': pipefilter,
220 220 }
221 221
222 222 def filter(s, cmd):
223 223 "filter a string through a command that transforms its input to its output"
224 224 for name, fn in filtertable.iteritems():
225 225 if cmd.startswith(name):
226 226 return fn(s, cmd[len(name):].lstrip())
227 227 return pipefilter(s, cmd)
228 228
229 229 def binary(s):
230 230 """return true if a string is binary data"""
231 231 return bool(s and '\0' in s)
232 232
233 233 def increasingchunks(source, min=1024, max=65536):
234 234 '''return no less than min bytes per chunk while data remains,
235 235 doubling min after each chunk until it reaches max'''
236 236 def log2(x):
237 237 if not x:
238 238 return 0
239 239 i = 0
240 240 while x:
241 241 x >>= 1
242 242 i += 1
243 243 return i - 1
244 244
245 245 buf = []
246 246 blen = 0
247 247 for chunk in source:
248 248 buf.append(chunk)
249 249 blen += len(chunk)
250 250 if blen >= min:
251 251 if min < max:
252 252 min = min << 1
253 253 nmin = 1 << log2(blen)
254 254 if nmin > min:
255 255 min = nmin
256 256 if min > max:
257 257 min = max
258 258 yield ''.join(buf)
259 259 blen = 0
260 260 buf = []
261 261 if buf:
262 262 yield ''.join(buf)
263 263
264 264 Abort = error.Abort
265 265
266 266 def always(fn):
267 267 return True
268 268
269 269 def never(fn):
270 270 return False
271 271
272 272 def pathto(root, n1, n2):
273 273 '''return the relative path from one place to another.
274 274 root should use os.sep to separate directories
275 275 n1 should use os.sep to separate directories
276 276 n2 should use "/" to separate directories
277 277 returns an os.sep-separated path.
278 278
279 279 If n1 is a relative path, it's assumed it's
280 280 relative to root.
281 281 n2 should always be relative to root.
282 282 '''
283 283 if not n1:
284 284 return localpath(n2)
285 285 if os.path.isabs(n1):
286 286 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
287 287 return os.path.join(root, localpath(n2))
288 288 n2 = '/'.join((pconvert(root), n2))
289 289 a, b = splitpath(n1), n2.split('/')
290 290 a.reverse()
291 291 b.reverse()
292 292 while a and b and a[-1] == b[-1]:
293 293 a.pop()
294 294 b.pop()
295 295 b.reverse()
296 296 return os.sep.join((['..'] * len(a)) + b) or '.'
297 297
298 298 _hgexecutable = None
299 299
300 300 def mainfrozen():
301 301 """return True if we are a frozen executable.
302 302
303 303 The code supports py2exe (most common, Windows only) and tools/freeze
304 304 (portable, not much used).
305 305 """
306 306 return (hasattr(sys, "frozen") or # new py2exe
307 307 hasattr(sys, "importers") or # old py2exe
308 308 imp.is_frozen("__main__")) # tools/freeze
309 309
310 310 def hgexecutable():
311 311 """return location of the 'hg' executable.
312 312
313 313 Defaults to $HG or 'hg' in the search path.
314 314 """
315 315 if _hgexecutable is None:
316 316 hg = os.environ.get('HG')
317 317 if hg:
318 318 _sethgexecutable(hg)
319 319 elif mainfrozen():
320 320 _sethgexecutable(sys.executable)
321 321 else:
322 322 exe = findexe('hg') or os.path.basename(sys.argv[0])
323 323 _sethgexecutable(exe)
324 324 return _hgexecutable
325 325
326 326 def _sethgexecutable(path):
327 327 """set location of the 'hg' executable"""
328 328 global _hgexecutable
329 329 _hgexecutable = path
330 330
331 331 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None, out=None):
332 332 '''enhanced shell command execution.
333 333 run with environment maybe modified, maybe in different dir.
334 334
335 335 if command fails and onerr is None, return status. if ui object,
336 336 print error message and return status, else raise onerr object as
337 337 exception.
338 338
339 339 if out is specified, it is assumed to be a file-like object that has a
340 340 write() method. stdout and stderr will be redirected to out.'''
341 341 try:
342 342 sys.stdout.flush()
343 343 except Exception:
344 344 pass
345 345 def py2shell(val):
346 346 'convert python object into string that is useful to shell'
347 347 if val is None or val is False:
348 348 return '0'
349 349 if val is True:
350 350 return '1'
351 351 return str(val)
352 352 origcmd = cmd
353 353 cmd = quotecommand(cmd)
354 354 env = dict(os.environ)
355 355 env.update((k, py2shell(v)) for k, v in environ.iteritems())
356 356 env['HG'] = hgexecutable()
357 357 if out is None or out == sys.__stdout__:
358 358 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
359 359 env=env, cwd=cwd)
360 360 else:
361 361 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
362 362 env=env, cwd=cwd, stdout=subprocess.PIPE,
363 363 stderr=subprocess.STDOUT)
364 364 for line in proc.stdout:
365 365 out.write(line)
366 366 proc.wait()
367 367 rc = proc.returncode
368 368 if sys.platform == 'OpenVMS' and rc & 1:
369 369 rc = 0
370 370 if rc and onerr:
371 371 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
372 372 explainexit(rc)[0])
373 373 if errprefix:
374 374 errmsg = '%s: %s' % (errprefix, errmsg)
375 375 try:
376 376 onerr.warn(errmsg + '\n')
377 377 except AttributeError:
378 378 raise onerr(errmsg)
379 379 return rc
380 380
381 381 def checksignature(func):
382 382 '''wrap a function with code to check for calling errors'''
383 383 def check(*args, **kwargs):
384 384 try:
385 385 return func(*args, **kwargs)
386 386 except TypeError:
387 387 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
388 388 raise error.SignatureError
389 389 raise
390 390
391 391 return check
392 392
393 393 def makedir(path, notindexed):
394 394 os.mkdir(path)
395 395
396 396 def unlinkpath(f):
397 397 """unlink and remove the directory if it is empty"""
398 398 os.unlink(f)
399 399 # try removing directories that might now be empty
400 400 try:
401 401 os.removedirs(os.path.dirname(f))
402 402 except OSError:
403 403 pass
404 404
405 405 def copyfile(src, dest):
406 406 "copy a file, preserving mode and atime/mtime"
407 407 if os.path.islink(src):
408 408 try:
409 409 os.unlink(dest)
410 410 except OSError:
411 411 pass
412 412 os.symlink(os.readlink(src), dest)
413 413 else:
414 414 try:
415 415 shutil.copyfile(src, dest)
416 416 shutil.copymode(src, dest)
417 417 except shutil.Error, inst:
418 418 raise Abort(str(inst))
419 419
420 420 def copyfiles(src, dst, hardlink=None):
421 421 """Copy a directory tree using hardlinks if possible"""
422 422
423 423 if hardlink is None:
424 424 hardlink = (os.stat(src).st_dev ==
425 425 os.stat(os.path.dirname(dst)).st_dev)
426 426
427 427 num = 0
428 428 if os.path.isdir(src):
429 429 os.mkdir(dst)
430 430 for name, kind in osutil.listdir(src):
431 431 srcname = os.path.join(src, name)
432 432 dstname = os.path.join(dst, name)
433 433 hardlink, n = copyfiles(srcname, dstname, hardlink)
434 434 num += n
435 435 else:
436 436 if hardlink:
437 437 try:
438 438 oslink(src, dst)
439 439 except (IOError, OSError):
440 440 hardlink = False
441 441 shutil.copy(src, dst)
442 442 else:
443 443 shutil.copy(src, dst)
444 444 num += 1
445 445
446 446 return hardlink, num
447 447
448 448 _winreservednames = '''con prn aux nul
449 449 com1 com2 com3 com4 com5 com6 com7 com8 com9
450 450 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split()
451 451 _winreservedchars = ':*?"<>|'
452 452 def checkwinfilename(path):
453 453 '''Check that the base-relative path is a valid filename on Windows.
454 454 Returns None if the path is ok, or a UI string describing the problem.
455 455
456 456 >>> checkwinfilename("just/a/normal/path")
457 457 >>> checkwinfilename("foo/bar/con.xml")
458 458 "filename contains 'con', which is reserved on Windows"
459 459 >>> checkwinfilename("foo/con.xml/bar")
460 460 "filename contains 'con', which is reserved on Windows"
461 461 >>> checkwinfilename("foo/bar/xml.con")
462 462 >>> checkwinfilename("foo/bar/AUX/bla.txt")
463 463 "filename contains 'AUX', which is reserved on Windows"
464 464 >>> checkwinfilename("foo/bar/bla:.txt")
465 465 "filename contains ':', which is reserved on Windows"
466 466 >>> checkwinfilename("foo/bar/b\07la.txt")
467 467 "filename contains '\\\\x07', which is invalid on Windows"
468 468 >>> checkwinfilename("foo/bar/bla ")
469 469 "filename ends with ' ', which is not allowed on Windows"
470 470 '''
471 471 for n in path.replace('\\', '/').split('/'):
472 472 if not n:
473 473 continue
474 474 for c in n:
475 475 if c in _winreservedchars:
476 476 return _("filename contains '%s', which is reserved "
477 477 "on Windows") % c
478 478 if ord(c) <= 31:
479 479 return _("filename contains %r, which is invalid "
480 480 "on Windows") % c
481 481 base = n.split('.')[0]
482 482 if base and base.lower() in _winreservednames:
483 483 return _("filename contains '%s', which is reserved "
484 484 "on Windows") % base
485 485 t = n[-1]
486 486 if t in '. ':
487 487 return _("filename ends with '%s', which is not allowed "
488 488 "on Windows") % t
489 489
490 490 def lookupreg(key, name=None, scope=None):
491 491 return None
492 492
493 493 def hidewindow():
494 494 """Hide current shell window.
495 495
496 496 Used to hide the window opened when starting asynchronous
497 497 child process under Windows, unneeded on other systems.
498 498 """
499 499 pass
500 500
501 501 if os.name == 'nt':
502 502 checkosfilename = checkwinfilename
503 503 from windows import *
504 504 else:
505 505 from posix import *
506 506
507 507 def makelock(info, pathname):
508 508 try:
509 509 return os.symlink(info, pathname)
510 510 except OSError, why:
511 511 if why.errno == errno.EEXIST:
512 512 raise
513 513 except AttributeError: # no symlink in os
514 514 pass
515 515
516 516 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
517 517 os.write(ld, info)
518 518 os.close(ld)
519 519
520 520 def readlock(pathname):
521 521 try:
522 522 return os.readlink(pathname)
523 523 except OSError, why:
524 524 if why.errno not in (errno.EINVAL, errno.ENOSYS):
525 525 raise
526 526 except AttributeError: # no symlink in os
527 527 pass
528 528 fp = posixfile(pathname)
529 529 r = fp.read()
530 530 fp.close()
531 531 return r
532 532
533 533 def fstat(fp):
534 534 '''stat file object that may not have fileno method.'''
535 535 try:
536 536 return os.fstat(fp.fileno())
537 537 except AttributeError:
538 538 return os.stat(fp.name)
539 539
540 540 # File system features
541 541
542 542 def checkcase(path):
543 543 """
544 544 Check whether the given path is on a case-sensitive filesystem
545 545
546 546 Requires a path (like /foo/.hg) ending with a foldable final
547 547 directory component.
548 548 """
549 549 s1 = os.stat(path)
550 550 d, b = os.path.split(path)
551 551 p2 = os.path.join(d, b.upper())
552 552 if path == p2:
553 553 p2 = os.path.join(d, b.lower())
554 554 try:
555 555 s2 = os.stat(p2)
556 556 if s2 == s1:
557 557 return False
558 558 return True
559 559 except OSError:
560 560 return True
561 561
562 562 _fspathcache = {}
563 563 def fspath(name, root):
564 564 '''Get name in the case stored in the filesystem
565 565
566 566 The name is either relative to root, or it is an absolute path starting
567 567 with root. Note that this function is unnecessary, and should not be
568 568 called, for case-sensitive filesystems (simply because it's expensive).
569 569 '''
570 570 # If name is absolute, make it relative
571 571 if name.lower().startswith(root.lower()):
572 572 l = len(root)
573 573 if name[l] == os.sep or name[l] == os.altsep:
574 574 l = l + 1
575 575 name = name[l:]
576 576
577 577 if not os.path.lexists(os.path.join(root, name)):
578 578 return None
579 579
580 580 seps = os.sep
581 581 if os.altsep:
582 582 seps = seps + os.altsep
583 583 # Protect backslashes. This gets silly very quickly.
584 584 seps.replace('\\','\\\\')
585 585 pattern = re.compile(r'([^%s]+)|([%s]+)' % (seps, seps))
586 586 dir = os.path.normcase(os.path.normpath(root))
587 587 result = []
588 588 for part, sep in pattern.findall(name):
589 589 if sep:
590 590 result.append(sep)
591 591 continue
592 592
593 593 if dir not in _fspathcache:
594 594 _fspathcache[dir] = os.listdir(dir)
595 595 contents = _fspathcache[dir]
596 596
597 597 lpart = part.lower()
598 598 lenp = len(part)
599 599 for n in contents:
600 600 if lenp == len(n) and n.lower() == lpart:
601 601 result.append(n)
602 602 break
603 603 else:
604 604 # Cannot happen, as the file exists!
605 605 result.append(part)
606 606 dir = os.path.join(dir, lpart)
607 607
608 608 return ''.join(result)
609 609
610 610 def checknlink(testfile):
611 611 '''check whether hardlink count reporting works properly'''
612 612
613 613 # testfile may be open, so we need a separate file for checking to
614 614 # work around issue2543 (or testfile may get lost on Samba shares)
615 615 f1 = testfile + ".hgtmp1"
616 616 if os.path.lexists(f1):
617 617 return False
618 618 try:
619 619 posixfile(f1, 'w').close()
620 620 except IOError:
621 621 return False
622 622
623 623 f2 = testfile + ".hgtmp2"
624 624 fd = None
625 625 try:
626 626 try:
627 627 oslink(f1, f2)
628 628 except OSError:
629 629 return False
630 630
631 631 # nlinks() may behave differently for files on Windows shares if
632 632 # the file is open.
633 633 fd = posixfile(f2)
634 634 return nlinks(f2) > 1
635 635 finally:
636 636 if fd is not None:
637 637 fd.close()
638 638 for f in (f1, f2):
639 639 try:
640 640 os.unlink(f)
641 641 except OSError:
642 642 pass
643 643
644 644 return False
645 645
646 646 def endswithsep(path):
647 647 '''Check path ends with os.sep or os.altsep.'''
648 648 return path.endswith(os.sep) or os.altsep and path.endswith(os.altsep)
649 649
650 650 def splitpath(path):
651 651 '''Split path by os.sep.
652 652 Note that this function does not use os.altsep because this is
653 653 an alternative of simple "xxx.split(os.sep)".
654 654 It is recommended to use os.path.normpath() before using this
655 655 function if need.'''
656 656 return path.split(os.sep)
657 657
658 658 def gui():
659 659 '''Are we running in a GUI?'''
660 660 if sys.platform == 'darwin':
661 661 if 'SSH_CONNECTION' in os.environ:
662 662 # handle SSH access to a box where the user is logged in
663 663 return False
664 664 elif getattr(osutil, 'isgui', None):
665 665 # check if a CoreGraphics session is available
666 666 return osutil.isgui()
667 667 else:
668 668 # pure build; use a safe default
669 669 return True
670 670 else:
671 671 return os.name == "nt" or os.environ.get("DISPLAY")
672 672
673 673 def mktempcopy(name, emptyok=False, createmode=None):
674 674 """Create a temporary file with the same contents from name
675 675
676 676 The permission bits are copied from the original file.
677 677
678 678 If the temporary file is going to be truncated immediately, you
679 679 can use emptyok=True as an optimization.
680 680
681 681 Returns the name of the temporary file.
682 682 """
683 683 d, fn = os.path.split(name)
684 684 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
685 685 os.close(fd)
686 686 # Temporary files are created with mode 0600, which is usually not
687 687 # what we want. If the original file already exists, just copy
688 688 # its mode. Otherwise, manually obey umask.
689 689 try:
690 690 st_mode = os.lstat(name).st_mode & 0777
691 691 except OSError, inst:
692 692 if inst.errno != errno.ENOENT:
693 693 raise
694 694 st_mode = createmode
695 695 if st_mode is None:
696 696 st_mode = ~umask
697 697 st_mode &= 0666
698 698 os.chmod(temp, st_mode)
699 699 if emptyok:
700 700 return temp
701 701 try:
702 702 try:
703 703 ifp = posixfile(name, "rb")
704 704 except IOError, inst:
705 705 if inst.errno == errno.ENOENT:
706 706 return temp
707 707 if not getattr(inst, 'filename', None):
708 708 inst.filename = name
709 709 raise
710 710 ofp = posixfile(temp, "wb")
711 711 for chunk in filechunkiter(ifp):
712 712 ofp.write(chunk)
713 713 ifp.close()
714 714 ofp.close()
715 715 except:
716 716 try: os.unlink(temp)
717 717 except: pass
718 718 raise
719 719 return temp
720 720
721 721 class atomictempfile(object):
722 722 '''writeable file object that atomically updates a file
723 723
724 724 All writes will go to a temporary copy of the original file. Call
725 725 rename() when you are done writing, and atomictempfile will rename
726 726 the temporary copy to the original name, making the changes visible.
727 727
728 728 Unlike other file-like objects, close() discards your writes by
729 729 simply deleting the temporary file.
730 730 '''
731 731 def __init__(self, name, mode='w+b', createmode=None):
732 732 self.__name = name # permanent name
733 733 self._tempname = mktempcopy(name, emptyok=('w' in mode),
734 734 createmode=createmode)
735 735 self._fp = posixfile(self._tempname, mode)
736 736
737 737 # delegated methods
738 738 self.write = self._fp.write
739 739 self.fileno = self._fp.fileno
740 740
741 741 def rename(self):
742 742 if not self._fp.closed:
743 743 self._fp.close()
744 744 rename(self._tempname, localpath(self.__name))
745 745
746 746 def close(self):
747 747 if not self._fp.closed:
748 748 try:
749 749 os.unlink(self._tempname)
750 750 except OSError:
751 751 pass
752 752 self._fp.close()
753 753
754 754 def __del__(self):
755 755 if hasattr(self, '_fp'): # constructor actually did something
756 756 self.close()
757 757
758 758 def makedirs(name, mode=None):
759 759 """recursive directory creation with parent mode inheritance"""
760 760 parent = os.path.abspath(os.path.dirname(name))
761 761 try:
762 762 os.mkdir(name)
763 763 if mode is not None:
764 764 os.chmod(name, mode)
765 765 return
766 766 except OSError, err:
767 767 if err.errno == errno.EEXIST:
768 768 return
769 769 if not name or parent == name or err.errno != errno.ENOENT:
770 770 raise
771 771 makedirs(parent, mode)
772 772 makedirs(name, mode)
773 773
774 774 def readfile(path):
775 775 fp = open(path, 'rb')
776 776 try:
777 777 return fp.read()
778 778 finally:
779 779 fp.close()
780 780
781 781 def writefile(path, text):
782 782 fp = open(path, 'wb')
783 783 try:
784 784 fp.write(text)
785 785 finally:
786 786 fp.close()
787 787
788 788 def appendfile(path, text):
789 789 fp = open(path, 'ab')
790 790 try:
791 791 fp.write(text)
792 792 finally:
793 793 fp.close()
794 794
795 795 class chunkbuffer(object):
796 796 """Allow arbitrary sized chunks of data to be efficiently read from an
797 797 iterator over chunks of arbitrary size."""
798 798
799 799 def __init__(self, in_iter):
800 800 """in_iter is the iterator that's iterating over the input chunks.
801 801 targetsize is how big a buffer to try to maintain."""
802 802 def splitbig(chunks):
803 803 for chunk in chunks:
804 804 if len(chunk) > 2**20:
805 805 pos = 0
806 806 while pos < len(chunk):
807 807 end = pos + 2 ** 18
808 808 yield chunk[pos:end]
809 809 pos = end
810 810 else:
811 811 yield chunk
812 812 self.iter = splitbig(in_iter)
813 813 self._queue = []
814 814
815 815 def read(self, l):
816 816 """Read L bytes of data from the iterator of chunks of data.
817 817 Returns less than L bytes if the iterator runs dry."""
818 818 left = l
819 819 buf = ''
820 820 queue = self._queue
821 821 while left > 0:
822 822 # refill the queue
823 823 if not queue:
824 824 target = 2**18
825 825 for chunk in self.iter:
826 826 queue.append(chunk)
827 827 target -= len(chunk)
828 828 if target <= 0:
829 829 break
830 830 if not queue:
831 831 break
832 832
833 833 chunk = queue.pop(0)
834 834 left -= len(chunk)
835 835 if left < 0:
836 836 queue.insert(0, chunk[left:])
837 837 buf += chunk[:left]
838 838 else:
839 839 buf += chunk
840 840
841 841 return buf
842 842
843 843 def filechunkiter(f, size=65536, limit=None):
844 844 """Create a generator that produces the data in the file size
845 845 (default 65536) bytes at a time, up to optional limit (default is
846 846 to read all data). Chunks may be less than size bytes if the
847 847 chunk is the last chunk in the file, or the file is a socket or
848 848 some other type of file that sometimes reads less data than is
849 849 requested."""
850 850 assert size >= 0
851 851 assert limit is None or limit >= 0
852 852 while True:
853 853 if limit is None:
854 854 nbytes = size
855 855 else:
856 856 nbytes = min(limit, size)
857 857 s = nbytes and f.read(nbytes)
858 858 if not s:
859 859 break
860 860 if limit:
861 861 limit -= len(s)
862 862 yield s
863 863
864 864 def makedate():
865 865 lt = time.localtime()
866 866 if lt[8] == 1 and time.daylight:
867 867 tz = time.altzone
868 868 else:
869 869 tz = time.timezone
870 870 t = time.mktime(lt)
871 871 if t < 0:
872 872 hint = _("check your clock")
873 873 raise Abort(_("negative timestamp: %d") % t, hint=hint)
874 874 return t, tz
875 875
876 876 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
877 877 """represent a (unixtime, offset) tuple as a localized time.
878 878 unixtime is seconds since the epoch, and offset is the time zone's
879 879 number of seconds away from UTC. if timezone is false, do not
880 880 append time zone to string."""
881 881 t, tz = date or makedate()
882 882 if t < 0:
883 883 t = 0 # time.gmtime(lt) fails on Windows for lt < -43200
884 884 tz = 0
885 885 if "%1" in format or "%2" in format:
886 886 sign = (tz > 0) and "-" or "+"
887 887 minutes = abs(tz) // 60
888 888 format = format.replace("%1", "%c%02d" % (sign, minutes // 60))
889 889 format = format.replace("%2", "%02d" % (minutes % 60))
890 890 s = time.strftime(format, time.gmtime(float(t) - tz))
891 891 return s
892 892
893 893 def shortdate(date=None):
894 894 """turn (timestamp, tzoff) tuple into iso 8631 date."""
895 895 return datestr(date, format='%Y-%m-%d')
896 896
897 897 def strdate(string, format, defaults=[]):
898 898 """parse a localized time string and return a (unixtime, offset) tuple.
899 899 if the string cannot be parsed, ValueError is raised."""
900 900 def timezone(string):
901 901 tz = string.split()[-1]
902 902 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
903 903 sign = (tz[0] == "+") and 1 or -1
904 904 hours = int(tz[1:3])
905 905 minutes = int(tz[3:5])
906 906 return -sign * (hours * 60 + minutes) * 60
907 907 if tz == "GMT" or tz == "UTC":
908 908 return 0
909 909 return None
910 910
911 911 # NOTE: unixtime = localunixtime + offset
912 912 offset, date = timezone(string), string
913 913 if offset is not None:
914 914 date = " ".join(string.split()[:-1])
915 915
916 916 # add missing elements from defaults
917 917 usenow = False # default to using biased defaults
918 918 for part in ("S", "M", "HI", "d", "mb", "yY"): # decreasing specificity
919 919 found = [True for p in part if ("%"+p) in format]
920 920 if not found:
921 921 date += "@" + defaults[part][usenow]
922 922 format += "@%" + part[0]
923 923 else:
924 924 # We've found a specific time element, less specific time
925 925 # elements are relative to today
926 926 usenow = True
927 927
928 928 timetuple = time.strptime(date, format)
929 929 localunixtime = int(calendar.timegm(timetuple))
930 930 if offset is None:
931 931 # local timezone
932 932 unixtime = int(time.mktime(timetuple))
933 933 offset = unixtime - localunixtime
934 934 else:
935 935 unixtime = localunixtime + offset
936 936 return unixtime, offset
937 937
938 938 def parsedate(date, formats=None, bias={}):
939 939 """parse a localized date/time and return a (unixtime, offset) tuple.
940 940
941 941 The date may be a "unixtime offset" string or in one of the specified
942 942 formats. If the date already is a (unixtime, offset) tuple, it is returned.
943 943 """
944 944 if not date:
945 945 return 0, 0
946 946 if isinstance(date, tuple) and len(date) == 2:
947 947 return date
948 948 if not formats:
949 949 formats = defaultdateformats
950 950 date = date.strip()
951 951 try:
952 952 when, offset = map(int, date.split(' '))
953 953 except ValueError:
954 954 # fill out defaults
955 955 now = makedate()
956 956 defaults = {}
957 957 for part in ("d", "mb", "yY", "HI", "M", "S"):
958 958 # this piece is for rounding the specific end of unknowns
959 959 b = bias.get(part)
960 960 if b is None:
961 961 if part[0] in "HMS":
962 962 b = "00"
963 963 else:
964 964 b = "0"
965 965
966 966 # this piece is for matching the generic end to today's date
967 967 n = datestr(now, "%" + part[0])
968 968
969 969 defaults[part] = (b, n)
970 970
971 971 for format in formats:
972 972 try:
973 973 when, offset = strdate(date, format, defaults)
974 974 except (ValueError, OverflowError):
975 975 pass
976 976 else:
977 977 break
978 978 else:
979 979 raise Abort(_('invalid date: %r') % date)
980 980 # validate explicit (probably user-specified) date and
981 981 # time zone offset. values must fit in signed 32 bits for
982 982 # current 32-bit linux runtimes. timezones go from UTC-12
983 983 # to UTC+14
984 984 if abs(when) > 0x7fffffff:
985 985 raise Abort(_('date exceeds 32 bits: %d') % when)
986 986 if when < 0:
987 987 raise Abort(_('negative date value: %d') % when)
988 988 if offset < -50400 or offset > 43200:
989 989 raise Abort(_('impossible time zone offset: %d') % offset)
990 990 return when, offset
991 991
992 992 def matchdate(date):
993 993 """Return a function that matches a given date match specifier
994 994
995 995 Formats include:
996 996
997 997 '{date}' match a given date to the accuracy provided
998 998
999 999 '<{date}' on or before a given date
1000 1000
1001 1001 '>{date}' on or after a given date
1002 1002
1003 1003 >>> p1 = parsedate("10:29:59")
1004 1004 >>> p2 = parsedate("10:30:00")
1005 1005 >>> p3 = parsedate("10:30:59")
1006 1006 >>> p4 = parsedate("10:31:00")
1007 1007 >>> p5 = parsedate("Sep 15 10:30:00 1999")
1008 1008 >>> f = matchdate("10:30")
1009 1009 >>> f(p1[0])
1010 1010 False
1011 1011 >>> f(p2[0])
1012 1012 True
1013 1013 >>> f(p3[0])
1014 1014 True
1015 1015 >>> f(p4[0])
1016 1016 False
1017 1017 >>> f(p5[0])
1018 1018 False
1019 1019 """
1020 1020
1021 1021 def lower(date):
1022 1022 d = dict(mb="1", d="1")
1023 1023 return parsedate(date, extendeddateformats, d)[0]
1024 1024
1025 1025 def upper(date):
1026 1026 d = dict(mb="12", HI="23", M="59", S="59")
1027 1027 for days in ("31", "30", "29"):
1028 1028 try:
1029 1029 d["d"] = days
1030 1030 return parsedate(date, extendeddateformats, d)[0]
1031 1031 except:
1032 1032 pass
1033 1033 d["d"] = "28"
1034 1034 return parsedate(date, extendeddateformats, d)[0]
1035 1035
1036 1036 date = date.strip()
1037 1037
1038 1038 if not date:
1039 1039 raise Abort(_("dates cannot consist entirely of whitespace"))
1040 1040 elif date[0] == "<":
1041 1041 if not date[1:]:
1042 1042 raise Abort(_("invalid day spec, use '<DATE'"))
1043 1043 when = upper(date[1:])
1044 1044 return lambda x: x <= when
1045 1045 elif date[0] == ">":
1046 1046 if not date[1:]:
1047 1047 raise Abort(_("invalid day spec, use '>DATE'"))
1048 1048 when = lower(date[1:])
1049 1049 return lambda x: x >= when
1050 1050 elif date[0] == "-":
1051 1051 try:
1052 1052 days = int(date[1:])
1053 1053 except ValueError:
1054 1054 raise Abort(_("invalid day spec: %s") % date[1:])
1055 1055 if days < 0:
1056 1056 raise Abort(_("%s must be nonnegative (see 'hg help dates')")
1057 1057 % date[1:])
1058 1058 when = makedate()[0] - days * 3600 * 24
1059 1059 return lambda x: x >= when
1060 1060 elif " to " in date:
1061 1061 a, b = date.split(" to ")
1062 1062 start, stop = lower(a), upper(b)
1063 1063 return lambda x: x >= start and x <= stop
1064 1064 else:
1065 1065 start, stop = lower(date), upper(date)
1066 1066 return lambda x: x >= start and x <= stop
1067 1067
1068 1068 def shortuser(user):
1069 1069 """Return a short representation of a user name or email address."""
1070 1070 f = user.find('@')
1071 1071 if f >= 0:
1072 1072 user = user[:f]
1073 1073 f = user.find('<')
1074 1074 if f >= 0:
1075 1075 user = user[f + 1:]
1076 1076 f = user.find(' ')
1077 1077 if f >= 0:
1078 1078 user = user[:f]
1079 1079 f = user.find('.')
1080 1080 if f >= 0:
1081 1081 user = user[:f]
1082 1082 return user
1083 1083
1084 1084 def email(author):
1085 1085 '''get email of author.'''
1086 1086 r = author.find('>')
1087 1087 if r == -1:
1088 1088 r = None
1089 1089 return author[author.find('<') + 1:r]
1090 1090
1091 1091 def _ellipsis(text, maxlength):
1092 1092 if len(text) <= maxlength:
1093 1093 return text, False
1094 1094 else:
1095 1095 return "%s..." % (text[:maxlength - 3]), True
1096 1096
1097 1097 def ellipsis(text, maxlength=400):
1098 1098 """Trim string to at most maxlength (default: 400) characters."""
1099 1099 try:
1100 1100 # use unicode not to split at intermediate multi-byte sequence
1101 1101 utext, truncated = _ellipsis(text.decode(encoding.encoding),
1102 1102 maxlength)
1103 1103 if not truncated:
1104 1104 return text
1105 1105 return utext.encode(encoding.encoding)
1106 1106 except (UnicodeDecodeError, UnicodeEncodeError):
1107 1107 return _ellipsis(text, maxlength)[0]
1108 1108
1109 1109 def bytecount(nbytes):
1110 1110 '''return byte count formatted as readable string, with units'''
1111 1111
1112 1112 units = (
1113 1113 (100, 1 << 30, _('%.0f GB')),
1114 1114 (10, 1 << 30, _('%.1f GB')),
1115 1115 (1, 1 << 30, _('%.2f GB')),
1116 1116 (100, 1 << 20, _('%.0f MB')),
1117 1117 (10, 1 << 20, _('%.1f MB')),
1118 1118 (1, 1 << 20, _('%.2f MB')),
1119 1119 (100, 1 << 10, _('%.0f KB')),
1120 1120 (10, 1 << 10, _('%.1f KB')),
1121 1121 (1, 1 << 10, _('%.2f KB')),
1122 1122 (1, 1, _('%.0f bytes')),
1123 1123 )
1124 1124
1125 1125 for multiplier, divisor, format in units:
1126 1126 if nbytes >= divisor * multiplier:
1127 1127 return format % (nbytes / float(divisor))
1128 1128 return units[-1][2] % nbytes
1129 1129
1130 1130 def uirepr(s):
1131 1131 # Avoid double backslash in Windows path repr()
1132 1132 return repr(s).replace('\\\\', '\\')
1133 1133
1134 1134 # delay import of textwrap
1135 1135 def MBTextWrapper(**kwargs):
1136 1136 class tw(textwrap.TextWrapper):
1137 1137 """
1138 1138 Extend TextWrapper for double-width characters.
1139 1139
1140 1140 Some Asian characters use two terminal columns instead of one.
1141 1141 A good example of this behavior can be seen with u'\u65e5\u672c',
1142 1142 the two Japanese characters for "Japan":
1143 1143 len() returns 2, but when printed to a terminal, they eat 4 columns.
1144 1144
1145 1145 (Note that this has nothing to do whatsoever with unicode
1146 1146 representation, or encoding of the underlying string)
1147 1147 """
1148 1148 def __init__(self, **kwargs):
1149 1149 textwrap.TextWrapper.__init__(self, **kwargs)
1150 1150
1151 1151 def _cutdown(self, str, space_left):
1152 1152 l = 0
1153 1153 ucstr = unicode(str, encoding.encoding)
1154 1154 colwidth = unicodedata.east_asian_width
1155 1155 for i in xrange(len(ucstr)):
1156 1156 l += colwidth(ucstr[i]) in 'WFA' and 2 or 1
1157 1157 if space_left < l:
1158 1158 return (ucstr[:i].encode(encoding.encoding),
1159 1159 ucstr[i:].encode(encoding.encoding))
1160 1160 return str, ''
1161 1161
1162 1162 # overriding of base class
1163 1163 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
1164 1164 space_left = max(width - cur_len, 1)
1165 1165
1166 1166 if self.break_long_words:
1167 1167 cut, res = self._cutdown(reversed_chunks[-1], space_left)
1168 1168 cur_line.append(cut)
1169 1169 reversed_chunks[-1] = res
1170 1170 elif not cur_line:
1171 1171 cur_line.append(reversed_chunks.pop())
1172 1172
1173 1173 global MBTextWrapper
1174 1174 MBTextWrapper = tw
1175 1175 return tw(**kwargs)
1176 1176
1177 1177 def wrap(line, width, initindent='', hangindent=''):
1178 1178 maxindent = max(len(hangindent), len(initindent))
1179 1179 if width <= maxindent:
1180 1180 # adjust for weird terminal size
1181 1181 width = max(78, maxindent + 1)
1182 1182 wrapper = MBTextWrapper(width=width,
1183 1183 initial_indent=initindent,
1184 1184 subsequent_indent=hangindent)
1185 1185 return wrapper.fill(line)
1186 1186
1187 1187 def iterlines(iterator):
1188 1188 for chunk in iterator:
1189 1189 for line in chunk.splitlines():
1190 1190 yield line
1191 1191
1192 1192 def expandpath(path):
1193 1193 return os.path.expanduser(os.path.expandvars(path))
1194 1194
1195 1195 def hgcmd():
1196 1196 """Return the command used to execute current hg
1197 1197
1198 1198 This is different from hgexecutable() because on Windows we want
1199 1199 to avoid things opening new shell windows like batch files, so we
1200 1200 get either the python call or current executable.
1201 1201 """
1202 1202 if mainfrozen():
1203 1203 return [sys.executable]
1204 1204 return gethgcmd()
1205 1205
1206 1206 def rundetached(args, condfn):
1207 1207 """Execute the argument list in a detached process.
1208 1208
1209 1209 condfn is a callable which is called repeatedly and should return
1210 1210 True once the child process is known to have started successfully.
1211 1211 At this point, the child process PID is returned. If the child
1212 1212 process fails to start or finishes before condfn() evaluates to
1213 1213 True, return -1.
1214 1214 """
1215 1215 # Windows case is easier because the child process is either
1216 1216 # successfully starting and validating the condition or exiting
1217 1217 # on failure. We just poll on its PID. On Unix, if the child
1218 1218 # process fails to start, it will be left in a zombie state until
1219 1219 # the parent wait on it, which we cannot do since we expect a long
1220 1220 # running process on success. Instead we listen for SIGCHLD telling
1221 1221 # us our child process terminated.
1222 1222 terminated = set()
1223 1223 def handler(signum, frame):
1224 1224 terminated.add(os.wait())
1225 1225 prevhandler = None
1226 1226 if hasattr(signal, 'SIGCHLD'):
1227 1227 prevhandler = signal.signal(signal.SIGCHLD, handler)
1228 1228 try:
1229 1229 pid = spawndetached(args)
1230 1230 while not condfn():
1231 1231 if ((pid in terminated or not testpid(pid))
1232 1232 and not condfn()):
1233 1233 return -1
1234 1234 time.sleep(0.1)
1235 1235 return pid
1236 1236 finally:
1237 1237 if prevhandler is not None:
1238 1238 signal.signal(signal.SIGCHLD, prevhandler)
1239 1239
1240 1240 try:
1241 1241 any, all = any, all
1242 1242 except NameError:
1243 1243 def any(iterable):
1244 1244 for i in iterable:
1245 1245 if i:
1246 1246 return True
1247 1247 return False
1248 1248
1249 1249 def all(iterable):
1250 1250 for i in iterable:
1251 1251 if not i:
1252 1252 return False
1253 1253 return True
1254 1254
1255 1255 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
1256 1256 """Return the result of interpolating items in the mapping into string s.
1257 1257
1258 1258 prefix is a single character string, or a two character string with
1259 1259 a backslash as the first character if the prefix needs to be escaped in
1260 1260 a regular expression.
1261 1261
1262 1262 fn is an optional function that will be applied to the replacement text
1263 1263 just before replacement.
1264 1264
1265 1265 escape_prefix is an optional flag that allows using doubled prefix for
1266 1266 its escaping.
1267 1267 """
1268 1268 fn = fn or (lambda s: s)
1269 1269 patterns = '|'.join(mapping.keys())
1270 1270 if escape_prefix:
1271 1271 patterns += '|' + prefix
1272 1272 if len(prefix) > 1:
1273 1273 prefix_char = prefix[1:]
1274 1274 else:
1275 1275 prefix_char = prefix
1276 1276 mapping[prefix_char] = prefix_char
1277 1277 r = re.compile(r'%s(%s)' % (prefix, patterns))
1278 1278 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
1279 1279
1280 1280 def getport(port):
1281 1281 """Return the port for a given network service.
1282 1282
1283 1283 If port is an integer, it's returned as is. If it's a string, it's
1284 1284 looked up using socket.getservbyname(). If there's no matching
1285 1285 service, util.Abort is raised.
1286 1286 """
1287 1287 try:
1288 1288 return int(port)
1289 1289 except ValueError:
1290 1290 pass
1291 1291
1292 1292 try:
1293 1293 return socket.getservbyname(port)
1294 1294 except socket.error:
1295 1295 raise Abort(_("no port number associated with service '%s'") % port)
1296 1296
1297 1297 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
1298 1298 '0': False, 'no': False, 'false': False, 'off': False,
1299 1299 'never': False}
1300 1300
1301 1301 def parsebool(s):
1302 1302 """Parse s into a boolean.
1303 1303
1304 1304 If s is not a valid boolean, returns None.
1305 1305 """
1306 1306 return _booleans.get(s.lower(), None)
1307 1307
1308 1308 _hexdig = '0123456789ABCDEFabcdef'
1309 1309 _hextochr = dict((a + b, chr(int(a + b, 16)))
1310 1310 for a in _hexdig for b in _hexdig)
1311 1311
1312 1312 def _urlunquote(s):
1313 1313 """unquote('abc%20def') -> 'abc def'."""
1314 1314 res = s.split('%')
1315 1315 # fastpath
1316 1316 if len(res) == 1:
1317 1317 return s
1318 1318 s = res[0]
1319 1319 for item in res[1:]:
1320 1320 try:
1321 1321 s += _hextochr[item[:2]] + item[2:]
1322 1322 except KeyError:
1323 1323 s += '%' + item
1324 1324 except UnicodeDecodeError:
1325 1325 s += unichr(int(item[:2], 16)) + item[2:]
1326 1326 return s
1327 1327
1328 1328 class url(object):
1329 1329 r"""Reliable URL parser.
1330 1330
1331 1331 This parses URLs and provides attributes for the following
1332 1332 components:
1333 1333
1334 1334 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
1335 1335
1336 1336 Missing components are set to None. The only exception is
1337 1337 fragment, which is set to '' if present but empty.
1338 1338
1339 1339 If parsefragment is False, fragment is included in query. If
1340 1340 parsequery is False, query is included in path. If both are
1341 1341 False, both fragment and query are included in path.
1342 1342
1343 1343 See http://www.ietf.org/rfc/rfc2396.txt for more information.
1344 1344
1345 1345 Note that for backward compatibility reasons, bundle URLs do not
1346 1346 take host names. That means 'bundle://../' has a path of '../'.
1347 1347
1348 1348 Examples:
1349 1349
1350 1350 >>> url('http://www.ietf.org/rfc/rfc2396.txt')
1351 1351 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
1352 1352 >>> url('ssh://[::1]:2200//home/joe/repo')
1353 1353 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
1354 1354 >>> url('file:///home/joe/repo')
1355 1355 <url scheme: 'file', path: '/home/joe/repo'>
1356 1356 >>> url('bundle:foo')
1357 1357 <url scheme: 'bundle', path: 'foo'>
1358 1358 >>> url('bundle://../foo')
1359 1359 <url scheme: 'bundle', path: '../foo'>
1360 1360 >>> url(r'c:\foo\bar')
1361 1361 <url path: 'c:\\foo\\bar'>
1362 1362 >>> url(r'\\blah\blah\blah')
1363 1363 <url path: '\\\\blah\\blah\\blah'>
1364 1364
1365 1365 Authentication credentials:
1366 1366
1367 1367 >>> url('ssh://joe:xyz@x/repo')
1368 1368 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
1369 1369 >>> url('ssh://joe@x/repo')
1370 1370 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
1371 1371
1372 1372 Query strings and fragments:
1373 1373
1374 1374 >>> url('http://host/a?b#c')
1375 1375 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
1376 1376 >>> url('http://host/a?b#c', parsequery=False, parsefragment=False)
1377 1377 <url scheme: 'http', host: 'host', path: 'a?b#c'>
1378 1378 """
1379 1379
1380 1380 _safechars = "!~*'()+"
1381 1381 _safepchars = "/!~*'()+"
1382 1382 _matchscheme = re.compile(r'^[a-zA-Z0-9+.\-]+:').match
1383 1383
1384 1384 def __init__(self, path, parsequery=True, parsefragment=True):
1385 1385 # We slowly chomp away at path until we have only the path left
1386 1386 self.scheme = self.user = self.passwd = self.host = None
1387 1387 self.port = self.path = self.query = self.fragment = None
1388 1388 self._localpath = True
1389 1389 self._hostport = ''
1390 1390 self._origpath = path
1391 1391
1392 1392 # special case for Windows drive letters and UNC paths
1393 1393 if hasdriveletter(path) or path.startswith(r'\\'):
1394 1394 self.path = path
1395 1395 return
1396 1396
1397 1397 # For compatibility reasons, we can't handle bundle paths as
1398 1398 # normal URLS
1399 1399 if path.startswith('bundle:'):
1400 1400 self.scheme = 'bundle'
1401 1401 path = path[7:]
1402 1402 if path.startswith('//'):
1403 1403 path = path[2:]
1404 1404 self.path = path
1405 1405 return
1406 1406
1407 1407 if self._matchscheme(path):
1408 1408 parts = path.split(':', 1)
1409 1409 if parts[0]:
1410 1410 self.scheme, path = parts
1411 1411 self._localpath = False
1412 1412
1413 1413 if not path:
1414 1414 path = None
1415 1415 if self._localpath:
1416 1416 self.path = ''
1417 1417 return
1418 1418 else:
1419 1419 if parsefragment and '#' in path:
1420 1420 path, self.fragment = path.split('#', 1)
1421 1421 if not path:
1422 1422 path = None
1423 1423 if self._localpath:
1424 1424 self.path = path
1425 1425 return
1426 1426
1427 1427 if parsequery and '?' in path:
1428 1428 path, self.query = path.split('?', 1)
1429 1429 if not path:
1430 1430 path = None
1431 1431 if not self.query:
1432 1432 self.query = None
1433 1433
1434 1434 # // is required to specify a host/authority
1435 1435 if path and path.startswith('//'):
1436 1436 parts = path[2:].split('/', 1)
1437 1437 if len(parts) > 1:
1438 1438 self.host, path = parts
1439 1439 path = path
1440 1440 else:
1441 1441 self.host = parts[0]
1442 1442 path = None
1443 1443 if not self.host:
1444 1444 self.host = None
1445 1445 if path:
1446 1446 path = '/' + path
1447 1447
1448 1448 if self.host and '@' in self.host:
1449 1449 self.user, self.host = self.host.rsplit('@', 1)
1450 1450 if ':' in self.user:
1451 1451 self.user, self.passwd = self.user.split(':', 1)
1452 1452 if not self.host:
1453 1453 self.host = None
1454 1454
1455 1455 # Don't split on colons in IPv6 addresses without ports
1456 1456 if (self.host and ':' in self.host and
1457 1457 not (self.host.startswith('[') and self.host.endswith(']'))):
1458 1458 self._hostport = self.host
1459 1459 self.host, self.port = self.host.rsplit(':', 1)
1460 1460 if not self.host:
1461 1461 self.host = None
1462 1462
1463 1463 if (self.host and self.scheme == 'file' and
1464 1464 self.host not in ('localhost', '127.0.0.1', '[::1]')):
1465 1465 raise Abort(_('file:// URLs can only refer to localhost'))
1466 1466
1467 1467 self.path = path
1468 1468
1469 1469 for a in ('user', 'passwd', 'host', 'port',
1470 1470 'path', 'query', 'fragment'):
1471 1471 v = getattr(self, a)
1472 1472 if v is not None:
1473 1473 setattr(self, a, _urlunquote(v))
1474 1474
1475 1475 def __repr__(self):
1476 1476 attrs = []
1477 1477 for a in ('scheme', 'user', 'passwd', 'host', 'port', 'path',
1478 1478 'query', 'fragment'):
1479 1479 v = getattr(self, a)
1480 1480 if v is not None:
1481 1481 attrs.append('%s: %r' % (a, v))
1482 1482 return '<url %s>' % ', '.join(attrs)
1483 1483
1484 1484 def __str__(self):
1485 1485 r"""Join the URL's components back into a URL string.
1486 1486
1487 1487 Examples:
1488 1488
1489 1489 >>> str(url('http://user:pw@host:80/?foo#bar'))
1490 1490 'http://user:pw@host:80/?foo#bar'
1491 1491 >>> str(url('ssh://user:pw@[::1]:2200//home/joe#'))
1492 1492 'ssh://user:pw@[::1]:2200//home/joe#'
1493 1493 >>> str(url('http://localhost:80//'))
1494 1494 'http://localhost:80//'
1495 1495 >>> str(url('http://localhost:80/'))
1496 1496 'http://localhost:80/'
1497 1497 >>> str(url('http://localhost:80'))
1498 1498 'http://localhost:80/'
1499 1499 >>> str(url('bundle:foo'))
1500 1500 'bundle:foo'
1501 1501 >>> str(url('bundle://../foo'))
1502 1502 'bundle:../foo'
1503 1503 >>> str(url('path'))
1504 1504 'path'
1505 1505 >>> str(url('file:///tmp/foo/bar'))
1506 1506 'file:///tmp/foo/bar'
1507 1507 >>> print url(r'bundle:foo\bar')
1508 1508 bundle:foo\bar
1509 1509 """
1510 1510 if self._localpath:
1511 1511 s = self.path
1512 1512 if self.scheme == 'bundle':
1513 1513 s = 'bundle:' + s
1514 1514 if self.fragment:
1515 1515 s += '#' + self.fragment
1516 1516 return s
1517 1517
1518 1518 s = self.scheme + ':'
1519 1519 if self.user or self.passwd or self.host:
1520 1520 s += '//'
1521 1521 elif self.scheme and (not self.path or self.path.startswith('/')):
1522 1522 s += '//'
1523 1523 if self.user:
1524 1524 s += urllib.quote(self.user, safe=self._safechars)
1525 1525 if self.passwd:
1526 1526 s += ':' + urllib.quote(self.passwd, safe=self._safechars)
1527 1527 if self.user or self.passwd:
1528 1528 s += '@'
1529 1529 if self.host:
1530 1530 if not (self.host.startswith('[') and self.host.endswith(']')):
1531 1531 s += urllib.quote(self.host)
1532 1532 else:
1533 1533 s += self.host
1534 1534 if self.port:
1535 1535 s += ':' + urllib.quote(self.port)
1536 1536 if self.host:
1537 1537 s += '/'
1538 1538 if self.path:
1539 1539 s += urllib.quote(self.path, safe=self._safepchars)
1540 1540 if self.query:
1541 1541 s += '?' + urllib.quote(self.query, safe=self._safepchars)
1542 1542 if self.fragment is not None:
1543 1543 s += '#' + urllib.quote(self.fragment, safe=self._safepchars)
1544 1544 return s
1545 1545
1546 1546 def authinfo(self):
1547 1547 user, passwd = self.user, self.passwd
1548 1548 try:
1549 1549 self.user, self.passwd = None, None
1550 1550 s = str(self)
1551 1551 finally:
1552 1552 self.user, self.passwd = user, passwd
1553 1553 if not self.user:
1554 1554 return (s, None)
1555 1555 return (s, (None, (str(self), self.host),
1556 1556 self.user, self.passwd or ''))
1557 1557
1558 def isabs(self):
1559 if self.scheme and self.scheme != 'file':
1560 return True # remote URL
1561 if hasdriveletter(self.path):
1562 return True # absolute for our purposes - can't be joined()
1563 if self.path.startswith(r'\\'):
1564 return True # Windows UNC path
1565 if self.path.startswith('/'):
1566 return True # POSIX-style
1567 return False
1568
1558 1569 def localpath(self):
1559 1570 if self.scheme == 'file' or self.scheme == 'bundle':
1560 1571 path = self.path or '/'
1561 1572 # For Windows, we need to promote hosts containing drive
1562 1573 # letters to paths with drive letters.
1563 1574 if hasdriveletter(self._hostport):
1564 1575 path = self._hostport + '/' + self.path
1565 1576 elif self.host is not None and self.path:
1566 1577 path = '/' + path
1567 1578 # We also need to handle the case of file:///C:/, which
1568 1579 # should return C:/, not /C:/.
1569 1580 elif hasdriveletter(path):
1570 1581 # Strip leading slash from paths with drive names
1571 1582 return path[1:]
1572 1583 return path
1573 1584 return self._origpath
1574 1585
1575 1586 def hasscheme(path):
1576 1587 return bool(url(path).scheme)
1577 1588
1578 1589 def hasdriveletter(path):
1579 1590 return path[1:2] == ':' and path[0:1].isalpha()
1580 1591
1581 1592 def localpath(path):
1582 1593 return url(path, parsequery=False, parsefragment=False).localpath()
1583 1594
1584 1595 def hidepassword(u):
1585 1596 '''hide user credential in a url string'''
1586 1597 u = url(u)
1587 1598 if u.passwd:
1588 1599 u.passwd = '***'
1589 1600 return str(u)
1590 1601
1591 1602 def removeauth(u):
1592 1603 '''remove all authentication information from a url string'''
1593 1604 u = url(u)
1594 1605 u.user = u.passwd = None
1595 1606 return str(u)
1596 1607
1597 1608 def isatty(fd):
1598 1609 try:
1599 1610 return fd.isatty()
1600 1611 except AttributeError:
1601 1612 return False
General Comments 0
You need to be logged in to leave comments. Login now