##// END OF EJS Templates
Fix issue483 - mq does not work under windows with gnu-win32 patch....
Patrick Mezard -
r4434:439b1c35 default
parent child Browse files
Show More
@@ -1,658 +1,661 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from i18n import _
9 9 from node import *
10 10 import base85, cmdutil, mdiff, util, context, revlog
11 11 import cStringIO, email.Parser, os, popen2, re, sha
12 12 import sys, tempfile, zlib
13 13
14 14 # helper functions
15 15
16 16 def copyfile(src, dst, basedir=None):
17 17 if not basedir:
18 18 basedir = os.getcwd()
19 19
20 20 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
21 21 if os.path.exists(absdst):
22 22 raise util.Abort(_("cannot create %s: destination already exists") %
23 23 dst)
24 24
25 25 targetdir = os.path.dirname(absdst)
26 26 if not os.path.isdir(targetdir):
27 27 os.makedirs(targetdir)
28 28
29 29 util.copyfile(abssrc, absdst)
30 30
31 31 # public functions
32 32
33 33 def extract(ui, fileobj):
34 34 '''extract patch from data read from fileobj.
35 35
36 36 patch can be a normal patch or contained in an email message.
37 37
38 38 return tuple (filename, message, user, date, node, p1, p2).
39 39 Any item in the returned tuple can be None. If filename is None,
40 40 fileobj did not contain a patch. Caller must unlink filename when done.'''
41 41
42 42 # attempt to detect the start of a patch
43 43 # (this heuristic is borrowed from quilt)
44 44 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
45 45 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
46 46 '(---|\*\*\*)[ \t])', re.MULTILINE)
47 47
48 48 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
49 49 tmpfp = os.fdopen(fd, 'w')
50 50 try:
51 51 msg = email.Parser.Parser().parse(fileobj)
52 52
53 53 message = msg['Subject']
54 54 user = msg['From']
55 55 # should try to parse msg['Date']
56 56 date = None
57 57 nodeid = None
58 58 parents = []
59 59
60 60 if message:
61 61 if message.startswith('[PATCH'):
62 62 pend = message.find(']')
63 63 if pend >= 0:
64 64 message = message[pend+1:].lstrip()
65 65 message = message.replace('\n\t', ' ')
66 66 ui.debug('Subject: %s\n' % message)
67 67 if user:
68 68 ui.debug('From: %s\n' % user)
69 69 diffs_seen = 0
70 70 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
71 71
72 72 for part in msg.walk():
73 73 content_type = part.get_content_type()
74 74 ui.debug('Content-Type: %s\n' % content_type)
75 75 if content_type not in ok_types:
76 76 continue
77 77 payload = part.get_payload(decode=True)
78 78 m = diffre.search(payload)
79 79 if m:
80 80 hgpatch = False
81 81 ignoretext = False
82 82
83 83 ui.debug(_('found patch at byte %d\n') % m.start(0))
84 84 diffs_seen += 1
85 85 cfp = cStringIO.StringIO()
86 86 if message:
87 87 cfp.write(message)
88 88 cfp.write('\n')
89 89 for line in payload[:m.start(0)].splitlines():
90 90 if line.startswith('# HG changeset patch'):
91 91 ui.debug(_('patch generated by hg export\n'))
92 92 hgpatch = True
93 93 # drop earlier commit message content
94 94 cfp.seek(0)
95 95 cfp.truncate()
96 96 elif hgpatch:
97 97 if line.startswith('# User '):
98 98 user = line[7:]
99 99 ui.debug('From: %s\n' % user)
100 100 elif line.startswith("# Date "):
101 101 date = line[7:]
102 102 elif line.startswith("# Node ID "):
103 103 nodeid = line[10:]
104 104 elif line.startswith("# Parent "):
105 105 parents.append(line[10:])
106 106 elif line == '---' and 'git-send-email' in msg['X-Mailer']:
107 107 ignoretext = True
108 108 if not line.startswith('# ') and not ignoretext:
109 109 cfp.write(line)
110 110 cfp.write('\n')
111 111 message = cfp.getvalue()
112 112 if tmpfp:
113 113 tmpfp.write(payload)
114 114 if not payload.endswith('\n'):
115 115 tmpfp.write('\n')
116 116 elif not diffs_seen and message and content_type == 'text/plain':
117 117 message += '\n' + payload
118 118 except:
119 119 tmpfp.close()
120 120 os.unlink(tmpname)
121 121 raise
122 122
123 123 tmpfp.close()
124 124 if not diffs_seen:
125 125 os.unlink(tmpname)
126 126 return None, message, user, date, None, None, None
127 127 p1 = parents and parents.pop(0) or None
128 128 p2 = parents and parents.pop(0) or None
129 129 return tmpname, message, user, date, nodeid, p1, p2
130 130
131 131 GP_PATCH = 1 << 0 # we have to run patch
132 132 GP_FILTER = 1 << 1 # there's some copy/rename operation
133 133 GP_BINARY = 1 << 2 # there's a binary patch
134 134
135 135 def readgitpatch(patchname):
136 136 """extract git-style metadata about patches from <patchname>"""
137 137 class gitpatch:
138 138 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
139 139 def __init__(self, path):
140 140 self.path = path
141 141 self.oldpath = None
142 142 self.mode = None
143 143 self.op = 'MODIFY'
144 144 self.copymod = False
145 145 self.lineno = 0
146 146 self.binary = False
147 147
148 148 # Filter patch for git information
149 149 gitre = re.compile('diff --git a/(.*) b/(.*)')
150 150 pf = file(patchname)
151 151 gp = None
152 152 gitpatches = []
153 153 # Can have a git patch with only metadata, causing patch to complain
154 154 dopatch = 0
155 155
156 156 lineno = 0
157 157 for line in pf:
158 158 lineno += 1
159 159 if line.startswith('diff --git'):
160 160 m = gitre.match(line)
161 161 if m:
162 162 if gp:
163 163 gitpatches.append(gp)
164 164 src, dst = m.group(1, 2)
165 165 gp = gitpatch(dst)
166 166 gp.lineno = lineno
167 167 elif gp:
168 168 if line.startswith('--- '):
169 169 if gp.op in ('COPY', 'RENAME'):
170 170 gp.copymod = True
171 171 dopatch |= GP_FILTER
172 172 gitpatches.append(gp)
173 173 gp = None
174 174 dopatch |= GP_PATCH
175 175 continue
176 176 if line.startswith('rename from '):
177 177 gp.op = 'RENAME'
178 178 gp.oldpath = line[12:].rstrip()
179 179 elif line.startswith('rename to '):
180 180 gp.path = line[10:].rstrip()
181 181 elif line.startswith('copy from '):
182 182 gp.op = 'COPY'
183 183 gp.oldpath = line[10:].rstrip()
184 184 elif line.startswith('copy to '):
185 185 gp.path = line[8:].rstrip()
186 186 elif line.startswith('deleted file'):
187 187 gp.op = 'DELETE'
188 188 elif line.startswith('new file mode '):
189 189 gp.op = 'ADD'
190 190 gp.mode = int(line.rstrip()[-3:], 8)
191 191 elif line.startswith('new mode '):
192 192 gp.mode = int(line.rstrip()[-3:], 8)
193 193 elif line.startswith('GIT binary patch'):
194 194 dopatch |= GP_BINARY
195 195 gp.binary = True
196 196 if gp:
197 197 gitpatches.append(gp)
198 198
199 199 if not gitpatches:
200 200 dopatch = GP_PATCH
201 201
202 202 return (dopatch, gitpatches)
203 203
204 204 def dogitpatch(patchname, gitpatches, cwd=None):
205 205 """Preprocess git patch so that vanilla patch can handle it"""
206 206 def extractbin(fp):
207 207 i = [0] # yuck
208 208 def readline():
209 209 i[0] += 1
210 210 return fp.readline().rstrip()
211 211 line = readline()
212 212 while line and not line.startswith('literal '):
213 213 line = readline()
214 214 if not line:
215 215 return None, i[0]
216 216 size = int(line[8:])
217 217 dec = []
218 218 line = readline()
219 219 while line:
220 220 l = line[0]
221 221 if l <= 'Z' and l >= 'A':
222 222 l = ord(l) - ord('A') + 1
223 223 else:
224 224 l = ord(l) - ord('a') + 27
225 225 dec.append(base85.b85decode(line[1:])[:l])
226 226 line = readline()
227 227 text = zlib.decompress(''.join(dec))
228 228 if len(text) != size:
229 229 raise util.Abort(_('binary patch is %d bytes, not %d') %
230 230 (len(text), size))
231 231 return text, i[0]
232 232
233 233 pf = file(patchname)
234 234 pfline = 1
235 235
236 236 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
237 237 tmpfp = os.fdopen(fd, 'w')
238 238
239 239 try:
240 240 for i in xrange(len(gitpatches)):
241 241 p = gitpatches[i]
242 242 if not p.copymod and not p.binary:
243 243 continue
244 244
245 245 # rewrite patch hunk
246 246 while pfline < p.lineno:
247 247 tmpfp.write(pf.readline())
248 248 pfline += 1
249 249
250 250 if p.binary:
251 251 text, delta = extractbin(pf)
252 252 if not text:
253 253 raise util.Abort(_('binary patch extraction failed'))
254 254 pfline += delta
255 255 if not cwd:
256 256 cwd = os.getcwd()
257 257 absdst = os.path.join(cwd, p.path)
258 258 basedir = os.path.dirname(absdst)
259 259 if not os.path.isdir(basedir):
260 260 os.makedirs(basedir)
261 261 out = file(absdst, 'wb')
262 262 out.write(text)
263 263 out.close()
264 264 elif p.copymod:
265 265 copyfile(p.oldpath, p.path, basedir=cwd)
266 266 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
267 267 line = pf.readline()
268 268 pfline += 1
269 269 while not line.startswith('--- a/'):
270 270 tmpfp.write(line)
271 271 line = pf.readline()
272 272 pfline += 1
273 273 tmpfp.write('--- a/%s\n' % p.path)
274 274
275 275 line = pf.readline()
276 276 while line:
277 277 tmpfp.write(line)
278 278 line = pf.readline()
279 279 except:
280 280 tmpfp.close()
281 281 os.unlink(patchname)
282 282 raise
283 283
284 284 tmpfp.close()
285 285 return patchname
286 286
287 287 def patch(patchname, ui, strip=1, cwd=None, files={}):
288 288 """apply the patch <patchname> to the working directory.
289 289 a list of patched files is returned"""
290 290
291 291 # helper function
292 292 def __patch(patchname):
293 293 """patch and updates the files and fuzz variables"""
294 294 fuzz = False
295 295
296 296 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''),
297 297 'patch')
298 298 args = []
299 if util.needbinarypatch():
300 args.append('--binary')
301
299 302 if cwd:
300 303 args.append('-d %s' % util.shellquote(cwd))
301 304 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
302 305 util.shellquote(patchname)))
303 306
304 307 for line in fp:
305 308 line = line.rstrip()
306 309 ui.note(line + '\n')
307 310 if line.startswith('patching file '):
308 311 pf = util.parse_patch_output(line)
309 312 printed_file = False
310 313 files.setdefault(pf, (None, None))
311 314 elif line.find('with fuzz') >= 0:
312 315 fuzz = True
313 316 if not printed_file:
314 317 ui.warn(pf + '\n')
315 318 printed_file = True
316 319 ui.warn(line + '\n')
317 320 elif line.find('saving rejects to file') >= 0:
318 321 ui.warn(line + '\n')
319 322 elif line.find('FAILED') >= 0:
320 323 if not printed_file:
321 324 ui.warn(pf + '\n')
322 325 printed_file = True
323 326 ui.warn(line + '\n')
324 327 code = fp.close()
325 328 if code:
326 329 raise util.Abort(_("patch command failed: %s") %
327 330 util.explain_exit(code)[0])
328 331 return fuzz
329 332
330 333 (dopatch, gitpatches) = readgitpatch(patchname)
331 334 for gp in gitpatches:
332 335 files[gp.path] = (gp.op, gp)
333 336
334 337 fuzz = False
335 338 if dopatch:
336 339 filterpatch = dopatch & (GP_FILTER | GP_BINARY)
337 340 if filterpatch:
338 341 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
339 342 try:
340 343 if dopatch & GP_PATCH:
341 344 fuzz = __patch(patchname)
342 345 finally:
343 346 if filterpatch:
344 347 os.unlink(patchname)
345 348
346 349 return fuzz
347 350
348 351 def diffopts(ui, opts={}, untrusted=False):
349 352 def get(key, name=None):
350 353 return (opts.get(key) or
351 354 ui.configbool('diff', name or key, None, untrusted=untrusted))
352 355 return mdiff.diffopts(
353 356 text=opts.get('text'),
354 357 git=get('git'),
355 358 nodates=get('nodates'),
356 359 showfunc=get('show_function', 'showfunc'),
357 360 ignorews=get('ignore_all_space', 'ignorews'),
358 361 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
359 362 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
360 363
361 364 def updatedir(ui, repo, patches, wlock=None):
362 365 '''Update dirstate after patch application according to metadata'''
363 366 if not patches:
364 367 return
365 368 copies = []
366 369 removes = {}
367 370 cfiles = patches.keys()
368 371 cwd = repo.getcwd()
369 372 if cwd:
370 373 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
371 374 for f in patches:
372 375 ctype, gp = patches[f]
373 376 if ctype == 'RENAME':
374 377 copies.append((gp.oldpath, gp.path, gp.copymod))
375 378 removes[gp.oldpath] = 1
376 379 elif ctype == 'COPY':
377 380 copies.append((gp.oldpath, gp.path, gp.copymod))
378 381 elif ctype == 'DELETE':
379 382 removes[gp.path] = 1
380 383 for src, dst, after in copies:
381 384 if not after:
382 385 copyfile(src, dst, repo.root)
383 386 repo.copy(src, dst, wlock=wlock)
384 387 removes = removes.keys()
385 388 if removes:
386 389 removes.sort()
387 390 repo.remove(removes, True, wlock=wlock)
388 391 for f in patches:
389 392 ctype, gp = patches[f]
390 393 if gp and gp.mode:
391 394 x = gp.mode & 0100 != 0
392 395 dst = os.path.join(repo.root, gp.path)
393 396 # patch won't create empty files
394 397 if ctype == 'ADD' and not os.path.exists(dst):
395 398 repo.wwrite(gp.path, '', x and 'x' or '')
396 399 else:
397 400 util.set_exec(dst, x)
398 401 cmdutil.addremove(repo, cfiles, wlock=wlock)
399 402 files = patches.keys()
400 403 files.extend([r for r in removes if r not in files])
401 404 files.sort()
402 405
403 406 return files
404 407
405 408 def b85diff(fp, to, tn):
406 409 '''print base85-encoded binary diff'''
407 410 def gitindex(text):
408 411 if not text:
409 412 return '0' * 40
410 413 l = len(text)
411 414 s = sha.new('blob %d\0' % l)
412 415 s.update(text)
413 416 return s.hexdigest()
414 417
415 418 def fmtline(line):
416 419 l = len(line)
417 420 if l <= 26:
418 421 l = chr(ord('A') + l - 1)
419 422 else:
420 423 l = chr(l - 26 + ord('a') - 1)
421 424 return '%c%s\n' % (l, base85.b85encode(line, True))
422 425
423 426 def chunk(text, csize=52):
424 427 l = len(text)
425 428 i = 0
426 429 while i < l:
427 430 yield text[i:i+csize]
428 431 i += csize
429 432
430 433 tohash = gitindex(to)
431 434 tnhash = gitindex(tn)
432 435 if tohash == tnhash:
433 436 return ""
434 437
435 438 # TODO: deltas
436 439 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
437 440 (tohash, tnhash, len(tn))]
438 441 for l in chunk(zlib.compress(tn)):
439 442 ret.append(fmtline(l))
440 443 ret.append('\n')
441 444 return ''.join(ret)
442 445
443 446 def diff(repo, node1=None, node2=None, files=None, match=util.always,
444 447 fp=None, changes=None, opts=None):
445 448 '''print diff of changes to files between two nodes, or node and
446 449 working directory.
447 450
448 451 if node1 is None, use first dirstate parent instead.
449 452 if node2 is None, compare node1 with working directory.'''
450 453
451 454 if opts is None:
452 455 opts = mdiff.defaultopts
453 456 if fp is None:
454 457 fp = repo.ui
455 458
456 459 if not node1:
457 460 node1 = repo.dirstate.parents()[0]
458 461
459 462 ccache = {}
460 463 def getctx(r):
461 464 if r not in ccache:
462 465 ccache[r] = context.changectx(repo, r)
463 466 return ccache[r]
464 467
465 468 flcache = {}
466 469 def getfilectx(f, ctx):
467 470 flctx = ctx.filectx(f, filelog=flcache.get(f))
468 471 if f not in flcache:
469 472 flcache[f] = flctx._filelog
470 473 return flctx
471 474
472 475 # reading the data for node1 early allows it to play nicely
473 476 # with repo.status and the revlog cache.
474 477 ctx1 = context.changectx(repo, node1)
475 478 # force manifest reading
476 479 man1 = ctx1.manifest()
477 480 date1 = util.datestr(ctx1.date())
478 481
479 482 if not changes:
480 483 changes = repo.status(node1, node2, files, match=match)[:5]
481 484 modified, added, removed, deleted, unknown = changes
482 485
483 486 if not modified and not added and not removed:
484 487 return
485 488
486 489 if node2:
487 490 ctx2 = context.changectx(repo, node2)
488 491 else:
489 492 ctx2 = context.workingctx(repo)
490 493 man2 = ctx2.manifest()
491 494
492 495 # returns False if there was no rename between ctx1 and ctx2
493 496 # returns None if the file was created between ctx1 and ctx2
494 497 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
495 498 def renamed(f):
496 499 startrev = ctx1.rev()
497 500 c = ctx2
498 501 crev = c.rev()
499 502 if crev is None:
500 503 crev = repo.changelog.count()
501 504 orig = f
502 505 while crev > startrev:
503 506 if f in c.files():
504 507 try:
505 508 src = getfilectx(f, c).renamed()
506 509 except revlog.LookupError:
507 510 return None
508 511 if src:
509 512 f = src[0]
510 513 crev = c.parents()[0].rev()
511 514 # try to reuse
512 515 c = getctx(crev)
513 516 if f not in man1:
514 517 return None
515 518 if f == orig:
516 519 return False
517 520 return f
518 521
519 522 if repo.ui.quiet:
520 523 r = None
521 524 else:
522 525 hexfunc = repo.ui.debugflag and hex or short
523 526 r = [hexfunc(node) for node in [node1, node2] if node]
524 527
525 528 if opts.git:
526 529 copied = {}
527 530 for f in added:
528 531 src = renamed(f)
529 532 if src:
530 533 copied[f] = src
531 534 srcs = [x[1] for x in copied.items()]
532 535
533 536 all = modified + added + removed
534 537 all.sort()
535 538 gone = {}
536 539
537 540 for f in all:
538 541 to = None
539 542 tn = None
540 543 dodiff = True
541 544 header = []
542 545 if f in man1:
543 546 to = getfilectx(f, ctx1).data()
544 547 if f not in removed:
545 548 tn = getfilectx(f, ctx2).data()
546 549 if opts.git:
547 550 def gitmode(x):
548 551 return x and '100755' or '100644'
549 552 def addmodehdr(header, omode, nmode):
550 553 if omode != nmode:
551 554 header.append('old mode %s\n' % omode)
552 555 header.append('new mode %s\n' % nmode)
553 556
554 557 a, b = f, f
555 558 if f in added:
556 559 mode = gitmode(man2.execf(f))
557 560 if f in copied:
558 561 a = copied[f]
559 562 omode = gitmode(man1.execf(a))
560 563 addmodehdr(header, omode, mode)
561 564 if a in removed and a not in gone:
562 565 op = 'rename'
563 566 gone[a] = 1
564 567 else:
565 568 op = 'copy'
566 569 header.append('%s from %s\n' % (op, a))
567 570 header.append('%s to %s\n' % (op, f))
568 571 to = getfilectx(a, ctx1).data()
569 572 else:
570 573 header.append('new file mode %s\n' % mode)
571 574 if util.binary(tn):
572 575 dodiff = 'binary'
573 576 elif f in removed:
574 577 if f in srcs:
575 578 dodiff = False
576 579 else:
577 580 mode = gitmode(man1.execf(f))
578 581 header.append('deleted file mode %s\n' % mode)
579 582 else:
580 583 omode = gitmode(man1.execf(f))
581 584 nmode = gitmode(man2.execf(f))
582 585 addmodehdr(header, omode, nmode)
583 586 if util.binary(to) or util.binary(tn):
584 587 dodiff = 'binary'
585 588 r = None
586 589 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
587 590 if dodiff:
588 591 if dodiff == 'binary':
589 592 text = b85diff(fp, to, tn)
590 593 else:
591 594 text = mdiff.unidiff(to, date1,
592 595 # ctx2 date may be dynamic
593 596 tn, util.datestr(ctx2.date()),
594 597 f, r, opts=opts)
595 598 if text or len(header) > 1:
596 599 fp.write(''.join(header))
597 600 fp.write(text)
598 601
599 602 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
600 603 opts=None):
601 604 '''export changesets as hg patches.'''
602 605
603 606 total = len(revs)
604 607 revwidth = max([len(str(rev)) for rev in revs])
605 608
606 609 def single(rev, seqno, fp):
607 610 ctx = repo.changectx(rev)
608 611 node = ctx.node()
609 612 parents = [p.node() for p in ctx.parents() if p]
610 613 if switch_parent:
611 614 parents.reverse()
612 615 prev = (parents and parents[0]) or nullid
613 616
614 617 if not fp:
615 618 fp = cmdutil.make_file(repo, template, node, total=total,
616 619 seqno=seqno, revwidth=revwidth)
617 620 if fp != sys.stdout and hasattr(fp, 'name'):
618 621 repo.ui.note("%s\n" % fp.name)
619 622
620 623 fp.write("# HG changeset patch\n")
621 624 fp.write("# User %s\n" % ctx.user())
622 625 fp.write("# Date %d %d\n" % ctx.date())
623 626 fp.write("# Node ID %s\n" % hex(node))
624 627 fp.write("# Parent %s\n" % hex(prev))
625 628 if len(parents) > 1:
626 629 fp.write("# Parent %s\n" % hex(parents[1]))
627 630 fp.write(ctx.description().rstrip())
628 631 fp.write("\n\n")
629 632
630 633 diff(repo, prev, node, fp=fp, opts=opts)
631 634 if fp not in (sys.stdout, repo.ui):
632 635 fp.close()
633 636
634 637 for seqno, rev in enumerate(revs):
635 638 single(rev, seqno+1, fp)
636 639
637 640 def diffstat(patchlines):
638 641 if not util.find_in_path('diffstat', os.environ.get('PATH', '')):
639 642 return
640 643 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
641 644 try:
642 645 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
643 646 try:
644 647 for line in patchlines: print >> p.tochild, line
645 648 p.tochild.close()
646 649 if p.wait(): return
647 650 fp = os.fdopen(fd, 'r')
648 651 stat = []
649 652 for line in fp: stat.append(line.lstrip())
650 653 last = stat.pop()
651 654 stat.insert(0, last)
652 655 stat = ''.join(stat)
653 656 if stat.startswith('0 files'): raise ValueError
654 657 return stat
655 658 except: raise
656 659 finally:
657 660 try: os.unlink(name)
658 661 except: pass
@@ -1,1527 +1,1531 b''
1 1 """
2 2 util.py - Mercurial utility functions and platform specfic implementations
3 3
4 4 Copyright 2005 K. Thananchayan <thananck@yahoo.com>
5 5 Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
6 6 Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
7 7
8 8 This software may be used and distributed according to the terms
9 9 of the GNU General Public License, incorporated herein by reference.
10 10
11 11 This contains helper routines that are independent of the SCM core and hide
12 12 platform-specific details from the core.
13 13 """
14 14
15 15 from i18n import _
16 16 import cStringIO, errno, getpass, popen2, re, shutil, sys, tempfile
17 17 import os, threading, time, calendar, ConfigParser, locale, glob
18 18
19 19 try:
20 20 _encoding = os.environ.get("HGENCODING") or locale.getpreferredencoding() \
21 21 or "ascii"
22 22 except locale.Error:
23 23 _encoding = 'ascii'
24 24 _encodingmode = os.environ.get("HGENCODINGMODE", "strict")
25 25 _fallbackencoding = 'ISO-8859-1'
26 26
27 27 def tolocal(s):
28 28 """
29 29 Convert a string from internal UTF-8 to local encoding
30 30
31 31 All internal strings should be UTF-8 but some repos before the
32 32 implementation of locale support may contain latin1 or possibly
33 33 other character sets. We attempt to decode everything strictly
34 34 using UTF-8, then Latin-1, and failing that, we use UTF-8 and
35 35 replace unknown characters.
36 36 """
37 37 for e in ('UTF-8', _fallbackencoding):
38 38 try:
39 39 u = s.decode(e) # attempt strict decoding
40 40 return u.encode(_encoding, "replace")
41 41 except LookupError, k:
42 42 raise Abort(_("%s, please check your locale settings") % k)
43 43 except UnicodeDecodeError:
44 44 pass
45 45 u = s.decode("utf-8", "replace") # last ditch
46 46 return u.encode(_encoding, "replace")
47 47
48 48 def fromlocal(s):
49 49 """
50 50 Convert a string from the local character encoding to UTF-8
51 51
52 52 We attempt to decode strings using the encoding mode set by
53 53 HG_ENCODINGMODE, which defaults to 'strict'. In this mode, unknown
54 54 characters will cause an error message. Other modes include
55 55 'replace', which replaces unknown characters with a special
56 56 Unicode character, and 'ignore', which drops the character.
57 57 """
58 58 try:
59 59 return s.decode(_encoding, _encodingmode).encode("utf-8")
60 60 except UnicodeDecodeError, inst:
61 61 sub = s[max(0, inst.start-10):inst.start+10]
62 62 raise Abort("decoding near '%s': %s!" % (sub, inst))
63 63 except LookupError, k:
64 64 raise Abort(_("%s, please check your locale settings") % k)
65 65
66 66 def locallen(s):
67 67 """Find the length in characters of a local string"""
68 68 return len(s.decode(_encoding, "replace"))
69 69
70 70 def localsub(s, a, b=None):
71 71 try:
72 72 u = s.decode(_encoding, _encodingmode)
73 73 if b is not None:
74 74 u = u[a:b]
75 75 else:
76 76 u = u[:a]
77 77 return u.encode(_encoding, _encodingmode)
78 78 except UnicodeDecodeError, inst:
79 79 sub = s[max(0, inst.start-10), inst.start+10]
80 80 raise Abort(_("decoding near '%s': %s!\n") % (sub, inst))
81 81
82 82 # used by parsedate
83 83 defaultdateformats = (
84 84 '%Y-%m-%d %H:%M:%S',
85 85 '%Y-%m-%d %I:%M:%S%p',
86 86 '%Y-%m-%d %H:%M',
87 87 '%Y-%m-%d %I:%M%p',
88 88 '%Y-%m-%d',
89 89 '%m-%d',
90 90 '%m/%d',
91 91 '%m/%d/%y',
92 92 '%m/%d/%Y',
93 93 '%a %b %d %H:%M:%S %Y',
94 94 '%a %b %d %I:%M:%S%p %Y',
95 95 '%b %d %H:%M:%S %Y',
96 96 '%b %d %I:%M:%S%p %Y',
97 97 '%b %d %H:%M:%S',
98 98 '%b %d %I:%M:%S%p',
99 99 '%b %d %H:%M',
100 100 '%b %d %I:%M%p',
101 101 '%b %d %Y',
102 102 '%b %d',
103 103 '%H:%M:%S',
104 104 '%I:%M:%SP',
105 105 '%H:%M',
106 106 '%I:%M%p',
107 107 )
108 108
109 109 extendeddateformats = defaultdateformats + (
110 110 "%Y",
111 111 "%Y-%m",
112 112 "%b",
113 113 "%b %Y",
114 114 )
115 115
116 116 class SignalInterrupt(Exception):
117 117 """Exception raised on SIGTERM and SIGHUP."""
118 118
119 119 # differences from SafeConfigParser:
120 120 # - case-sensitive keys
121 121 # - allows values that are not strings (this means that you may not
122 122 # be able to save the configuration to a file)
123 123 class configparser(ConfigParser.SafeConfigParser):
124 124 def optionxform(self, optionstr):
125 125 return optionstr
126 126
127 127 def set(self, section, option, value):
128 128 return ConfigParser.ConfigParser.set(self, section, option, value)
129 129
130 130 def _interpolate(self, section, option, rawval, vars):
131 131 if not isinstance(rawval, basestring):
132 132 return rawval
133 133 return ConfigParser.SafeConfigParser._interpolate(self, section,
134 134 option, rawval, vars)
135 135
136 136 def cachefunc(func):
137 137 '''cache the result of function calls'''
138 138 # XXX doesn't handle keywords args
139 139 cache = {}
140 140 if func.func_code.co_argcount == 1:
141 141 # we gain a small amount of time because
142 142 # we don't need to pack/unpack the list
143 143 def f(arg):
144 144 if arg not in cache:
145 145 cache[arg] = func(arg)
146 146 return cache[arg]
147 147 else:
148 148 def f(*args):
149 149 if args not in cache:
150 150 cache[args] = func(*args)
151 151 return cache[args]
152 152
153 153 return f
154 154
155 155 def pipefilter(s, cmd):
156 156 '''filter string S through command CMD, returning its output'''
157 157 (pout, pin) = popen2.popen2(cmd, -1, 'b')
158 158 def writer():
159 159 try:
160 160 pin.write(s)
161 161 pin.close()
162 162 except IOError, inst:
163 163 if inst.errno != errno.EPIPE:
164 164 raise
165 165
166 166 # we should use select instead on UNIX, but this will work on most
167 167 # systems, including Windows
168 168 w = threading.Thread(target=writer)
169 169 w.start()
170 170 f = pout.read()
171 171 pout.close()
172 172 w.join()
173 173 return f
174 174
175 175 def tempfilter(s, cmd):
176 176 '''filter string S through a pair of temporary files with CMD.
177 177 CMD is used as a template to create the real command to be run,
178 178 with the strings INFILE and OUTFILE replaced by the real names of
179 179 the temporary files generated.'''
180 180 inname, outname = None, None
181 181 try:
182 182 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
183 183 fp = os.fdopen(infd, 'wb')
184 184 fp.write(s)
185 185 fp.close()
186 186 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
187 187 os.close(outfd)
188 188 cmd = cmd.replace('INFILE', inname)
189 189 cmd = cmd.replace('OUTFILE', outname)
190 190 code = os.system(cmd)
191 191 if code: raise Abort(_("command '%s' failed: %s") %
192 192 (cmd, explain_exit(code)))
193 193 return open(outname, 'rb').read()
194 194 finally:
195 195 try:
196 196 if inname: os.unlink(inname)
197 197 except: pass
198 198 try:
199 199 if outname: os.unlink(outname)
200 200 except: pass
201 201
202 202 filtertable = {
203 203 'tempfile:': tempfilter,
204 204 'pipe:': pipefilter,
205 205 }
206 206
207 207 def filter(s, cmd):
208 208 "filter a string through a command that transforms its input to its output"
209 209 for name, fn in filtertable.iteritems():
210 210 if cmd.startswith(name):
211 211 return fn(s, cmd[len(name):].lstrip())
212 212 return pipefilter(s, cmd)
213 213
214 214 def binary(s):
215 215 """return true if a string is binary data using diff's heuristic"""
216 216 if s and '\0' in s[:4096]:
217 217 return True
218 218 return False
219 219
220 220 def unique(g):
221 221 """return the uniq elements of iterable g"""
222 222 seen = {}
223 223 l = []
224 224 for f in g:
225 225 if f not in seen:
226 226 seen[f] = 1
227 227 l.append(f)
228 228 return l
229 229
230 230 class Abort(Exception):
231 231 """Raised if a command needs to print an error and exit."""
232 232
233 233 class UnexpectedOutput(Abort):
234 234 """Raised to print an error with part of output and exit."""
235 235
236 236 def always(fn): return True
237 237 def never(fn): return False
238 238
239 239 def expand_glob(pats):
240 240 '''On Windows, expand the implicit globs in a list of patterns'''
241 241 if os.name != 'nt':
242 242 return list(pats)
243 243 ret = []
244 244 for p in pats:
245 245 kind, name = patkind(p, None)
246 246 if kind is None:
247 247 globbed = glob.glob(name)
248 248 if globbed:
249 249 ret.extend(globbed)
250 250 continue
251 251 # if we couldn't expand the glob, just keep it around
252 252 ret.append(p)
253 253 return ret
254 254
255 255 def patkind(name, dflt_pat='glob'):
256 256 """Split a string into an optional pattern kind prefix and the
257 257 actual pattern."""
258 258 for prefix in 're', 'glob', 'path', 'relglob', 'relpath', 'relre':
259 259 if name.startswith(prefix + ':'): return name.split(':', 1)
260 260 return dflt_pat, name
261 261
262 262 def globre(pat, head='^', tail='$'):
263 263 "convert a glob pattern into a regexp"
264 264 i, n = 0, len(pat)
265 265 res = ''
266 266 group = False
267 267 def peek(): return i < n and pat[i]
268 268 while i < n:
269 269 c = pat[i]
270 270 i = i+1
271 271 if c == '*':
272 272 if peek() == '*':
273 273 i += 1
274 274 res += '.*'
275 275 else:
276 276 res += '[^/]*'
277 277 elif c == '?':
278 278 res += '.'
279 279 elif c == '[':
280 280 j = i
281 281 if j < n and pat[j] in '!]':
282 282 j += 1
283 283 while j < n and pat[j] != ']':
284 284 j += 1
285 285 if j >= n:
286 286 res += '\\['
287 287 else:
288 288 stuff = pat[i:j].replace('\\','\\\\')
289 289 i = j + 1
290 290 if stuff[0] == '!':
291 291 stuff = '^' + stuff[1:]
292 292 elif stuff[0] == '^':
293 293 stuff = '\\' + stuff
294 294 res = '%s[%s]' % (res, stuff)
295 295 elif c == '{':
296 296 group = True
297 297 res += '(?:'
298 298 elif c == '}' and group:
299 299 res += ')'
300 300 group = False
301 301 elif c == ',' and group:
302 302 res += '|'
303 303 elif c == '\\':
304 304 p = peek()
305 305 if p:
306 306 i += 1
307 307 res += re.escape(p)
308 308 else:
309 309 res += re.escape(c)
310 310 else:
311 311 res += re.escape(c)
312 312 return head + res + tail
313 313
314 314 _globchars = {'[': 1, '{': 1, '*': 1, '?': 1}
315 315
316 316 def pathto(root, n1, n2):
317 317 '''return the relative path from one place to another.
318 318 root should use os.sep to separate directories
319 319 n1 should use os.sep to separate directories
320 320 n2 should use "/" to separate directories
321 321 returns an os.sep-separated path.
322 322
323 323 If n1 is a relative path, it's assumed it's
324 324 relative to root.
325 325 n2 should always be relative to root.
326 326 '''
327 327 if not n1: return localpath(n2)
328 328 if os.path.isabs(n1):
329 329 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
330 330 return os.path.join(root, localpath(n2))
331 331 n2 = '/'.join((pconvert(root), n2))
332 332 a, b = n1.split(os.sep), n2.split('/')
333 333 a.reverse()
334 334 b.reverse()
335 335 while a and b and a[-1] == b[-1]:
336 336 a.pop()
337 337 b.pop()
338 338 b.reverse()
339 339 return os.sep.join((['..'] * len(a)) + b)
340 340
341 341 def canonpath(root, cwd, myname):
342 342 """return the canonical path of myname, given cwd and root"""
343 343 if root == os.sep:
344 344 rootsep = os.sep
345 345 elif root.endswith(os.sep):
346 346 rootsep = root
347 347 else:
348 348 rootsep = root + os.sep
349 349 name = myname
350 350 if not os.path.isabs(name):
351 351 name = os.path.join(root, cwd, name)
352 352 name = os.path.normpath(name)
353 353 if name != rootsep and name.startswith(rootsep):
354 354 name = name[len(rootsep):]
355 355 audit_path(name)
356 356 return pconvert(name)
357 357 elif name == root:
358 358 return ''
359 359 else:
360 360 # Determine whether `name' is in the hierarchy at or beneath `root',
361 361 # by iterating name=dirname(name) until that causes no change (can't
362 362 # check name == '/', because that doesn't work on windows). For each
363 363 # `name', compare dev/inode numbers. If they match, the list `rel'
364 364 # holds the reversed list of components making up the relative file
365 365 # name we want.
366 366 root_st = os.stat(root)
367 367 rel = []
368 368 while True:
369 369 try:
370 370 name_st = os.stat(name)
371 371 except OSError:
372 372 break
373 373 if samestat(name_st, root_st):
374 374 if not rel:
375 375 # name was actually the same as root (maybe a symlink)
376 376 return ''
377 377 rel.reverse()
378 378 name = os.path.join(*rel)
379 379 audit_path(name)
380 380 return pconvert(name)
381 381 dirname, basename = os.path.split(name)
382 382 rel.append(basename)
383 383 if dirname == name:
384 384 break
385 385 name = dirname
386 386
387 387 raise Abort('%s not under root' % myname)
388 388
389 389 def matcher(canonroot, cwd='', names=[], inc=[], exc=[], src=None):
390 390 return _matcher(canonroot, cwd, names, inc, exc, 'glob', src)
391 391
392 392 def cmdmatcher(canonroot, cwd='', names=[], inc=[], exc=[], src=None,
393 393 globbed=False, default=None):
394 394 default = default or 'relpath'
395 395 if default == 'relpath' and not globbed:
396 396 names = expand_glob(names)
397 397 return _matcher(canonroot, cwd, names, inc, exc, default, src)
398 398
399 399 def _matcher(canonroot, cwd, names, inc, exc, dflt_pat, src):
400 400 """build a function to match a set of file patterns
401 401
402 402 arguments:
403 403 canonroot - the canonical root of the tree you're matching against
404 404 cwd - the current working directory, if relevant
405 405 names - patterns to find
406 406 inc - patterns to include
407 407 exc - patterns to exclude
408 408 dflt_pat - if a pattern in names has no explicit type, assume this one
409 409 src - where these patterns came from (e.g. .hgignore)
410 410
411 411 a pattern is one of:
412 412 'glob:<glob>' - a glob relative to cwd
413 413 're:<regexp>' - a regular expression
414 414 'path:<path>' - a path relative to canonroot
415 415 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
416 416 'relpath:<path>' - a path relative to cwd
417 417 'relre:<regexp>' - a regexp that doesn't have to match the start of a name
418 418 '<something>' - one of the cases above, selected by the dflt_pat argument
419 419
420 420 returns:
421 421 a 3-tuple containing
422 422 - list of roots (places where one should start a recursive walk of the fs);
423 423 this often matches the explicit non-pattern names passed in, but also
424 424 includes the initial part of glob: patterns that has no glob characters
425 425 - a bool match(filename) function
426 426 - a bool indicating if any patterns were passed in
427 427 """
428 428
429 429 # a common case: no patterns at all
430 430 if not names and not inc and not exc:
431 431 return [], always, False
432 432
433 433 def contains_glob(name):
434 434 for c in name:
435 435 if c in _globchars: return True
436 436 return False
437 437
438 438 def regex(kind, name, tail):
439 439 '''convert a pattern into a regular expression'''
440 440 if not name:
441 441 return ''
442 442 if kind == 're':
443 443 return name
444 444 elif kind == 'path':
445 445 return '^' + re.escape(name) + '(?:/|$)'
446 446 elif kind == 'relglob':
447 447 return globre(name, '(?:|.*/)', tail)
448 448 elif kind == 'relpath':
449 449 return re.escape(name) + '(?:/|$)'
450 450 elif kind == 'relre':
451 451 if name.startswith('^'):
452 452 return name
453 453 return '.*' + name
454 454 return globre(name, '', tail)
455 455
456 456 def matchfn(pats, tail):
457 457 """build a matching function from a set of patterns"""
458 458 if not pats:
459 459 return
460 460 try:
461 461 pat = '(?:%s)' % '|'.join([regex(k, p, tail) for (k, p) in pats])
462 462 return re.compile(pat).match
463 463 except re.error:
464 464 for k, p in pats:
465 465 try:
466 466 re.compile('(?:%s)' % regex(k, p, tail))
467 467 except re.error:
468 468 if src:
469 469 raise Abort("%s: invalid pattern (%s): %s" %
470 470 (src, k, p))
471 471 else:
472 472 raise Abort("invalid pattern (%s): %s" % (k, p))
473 473 raise Abort("invalid pattern")
474 474
475 475 def globprefix(pat):
476 476 '''return the non-glob prefix of a path, e.g. foo/* -> foo'''
477 477 root = []
478 478 for p in pat.split('/'):
479 479 if contains_glob(p): break
480 480 root.append(p)
481 481 return '/'.join(root) or '.'
482 482
483 483 def normalizepats(names, default):
484 484 pats = []
485 485 roots = []
486 486 anypats = False
487 487 for kind, name in [patkind(p, default) for p in names]:
488 488 if kind in ('glob', 'relpath'):
489 489 name = canonpath(canonroot, cwd, name)
490 490 elif kind in ('relglob', 'path'):
491 491 name = normpath(name)
492 492
493 493 pats.append((kind, name))
494 494
495 495 if kind in ('glob', 're', 'relglob', 'relre'):
496 496 anypats = True
497 497
498 498 if kind == 'glob':
499 499 root = globprefix(name)
500 500 roots.append(root)
501 501 elif kind in ('relpath', 'path'):
502 502 roots.append(name or '.')
503 503 elif kind == 'relglob':
504 504 roots.append('.')
505 505 return roots, pats, anypats
506 506
507 507 roots, pats, anypats = normalizepats(names, dflt_pat)
508 508
509 509 patmatch = matchfn(pats, '$') or always
510 510 incmatch = always
511 511 if inc:
512 512 dummy, inckinds, dummy = normalizepats(inc, 'glob')
513 513 incmatch = matchfn(inckinds, '(?:/|$)')
514 514 excmatch = lambda fn: False
515 515 if exc:
516 516 dummy, exckinds, dummy = normalizepats(exc, 'glob')
517 517 excmatch = matchfn(exckinds, '(?:/|$)')
518 518
519 519 if not names and inc and not exc:
520 520 # common case: hgignore patterns
521 521 match = incmatch
522 522 else:
523 523 match = lambda fn: incmatch(fn) and not excmatch(fn) and patmatch(fn)
524 524
525 525 return (roots, match, (inc or exc or anypats) and True)
526 526
527 527 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None):
528 528 '''enhanced shell command execution.
529 529 run with environment maybe modified, maybe in different dir.
530 530
531 531 if command fails and onerr is None, return status. if ui object,
532 532 print error message and return status, else raise onerr object as
533 533 exception.'''
534 534 def py2shell(val):
535 535 'convert python object into string that is useful to shell'
536 536 if val in (None, False):
537 537 return '0'
538 538 if val == True:
539 539 return '1'
540 540 return str(val)
541 541 oldenv = {}
542 542 for k in environ:
543 543 oldenv[k] = os.environ.get(k)
544 544 if cwd is not None:
545 545 oldcwd = os.getcwd()
546 546 origcmd = cmd
547 547 if os.name == 'nt':
548 548 cmd = '"%s"' % cmd
549 549 try:
550 550 for k, v in environ.iteritems():
551 551 os.environ[k] = py2shell(v)
552 552 if cwd is not None and oldcwd != cwd:
553 553 os.chdir(cwd)
554 554 rc = os.system(cmd)
555 555 if rc and onerr:
556 556 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
557 557 explain_exit(rc)[0])
558 558 if errprefix:
559 559 errmsg = '%s: %s' % (errprefix, errmsg)
560 560 try:
561 561 onerr.warn(errmsg + '\n')
562 562 except AttributeError:
563 563 raise onerr(errmsg)
564 564 return rc
565 565 finally:
566 566 for k, v in oldenv.iteritems():
567 567 if v is None:
568 568 del os.environ[k]
569 569 else:
570 570 os.environ[k] = v
571 571 if cwd is not None and oldcwd != cwd:
572 572 os.chdir(oldcwd)
573 573
574 574 # os.path.lexists is not available on python2.3
575 575 def lexists(filename):
576 576 "test whether a file with this name exists. does not follow symlinks"
577 577 try:
578 578 os.lstat(filename)
579 579 except:
580 580 return False
581 581 return True
582 582
583 583 def rename(src, dst):
584 584 """forcibly rename a file"""
585 585 try:
586 586 os.rename(src, dst)
587 587 except OSError, err:
588 588 # on windows, rename to existing file is not allowed, so we
589 589 # must delete destination first. but if file is open, unlink
590 590 # schedules it for delete but does not delete it. rename
591 591 # happens immediately even for open files, so we create
592 592 # temporary file, delete it, rename destination to that name,
593 593 # then delete that. then rename is safe to do.
594 594 fd, temp = tempfile.mkstemp(dir=os.path.dirname(dst) or '.')
595 595 os.close(fd)
596 596 os.unlink(temp)
597 597 os.rename(dst, temp)
598 598 os.unlink(temp)
599 599 os.rename(src, dst)
600 600
601 601 def unlink(f):
602 602 """unlink and remove the directory if it is empty"""
603 603 os.unlink(f)
604 604 # try removing directories that might now be empty
605 605 try:
606 606 os.removedirs(os.path.dirname(f))
607 607 except OSError:
608 608 pass
609 609
610 610 def copyfile(src, dest):
611 611 "copy a file, preserving mode"
612 612 if os.path.islink(src):
613 613 try:
614 614 os.unlink(dest)
615 615 except:
616 616 pass
617 617 os.symlink(os.readlink(src), dest)
618 618 else:
619 619 try:
620 620 shutil.copyfile(src, dest)
621 621 shutil.copymode(src, dest)
622 622 except shutil.Error, inst:
623 623 raise Abort(str(inst))
624 624
625 625 def copyfiles(src, dst, hardlink=None):
626 626 """Copy a directory tree using hardlinks if possible"""
627 627
628 628 if hardlink is None:
629 629 hardlink = (os.stat(src).st_dev ==
630 630 os.stat(os.path.dirname(dst)).st_dev)
631 631
632 632 if os.path.isdir(src):
633 633 os.mkdir(dst)
634 634 for name in os.listdir(src):
635 635 srcname = os.path.join(src, name)
636 636 dstname = os.path.join(dst, name)
637 637 copyfiles(srcname, dstname, hardlink)
638 638 else:
639 639 if hardlink:
640 640 try:
641 641 os_link(src, dst)
642 642 except (IOError, OSError):
643 643 hardlink = False
644 644 shutil.copy(src, dst)
645 645 else:
646 646 shutil.copy(src, dst)
647 647
648 648 def audit_path(path):
649 649 """Abort if path contains dangerous components"""
650 650 parts = os.path.normcase(path).split(os.sep)
651 651 if (os.path.splitdrive(path)[0] or parts[0] in ('.hg', '')
652 652 or os.pardir in parts):
653 653 raise Abort(_("path contains illegal component: %s\n") % path)
654 654
655 655 def _makelock_file(info, pathname):
656 656 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
657 657 os.write(ld, info)
658 658 os.close(ld)
659 659
660 660 def _readlock_file(pathname):
661 661 return posixfile(pathname).read()
662 662
663 663 def nlinks(pathname):
664 664 """Return number of hardlinks for the given file."""
665 665 return os.lstat(pathname).st_nlink
666 666
667 667 if hasattr(os, 'link'):
668 668 os_link = os.link
669 669 else:
670 670 def os_link(src, dst):
671 671 raise OSError(0, _("Hardlinks not supported"))
672 672
673 673 def fstat(fp):
674 674 '''stat file object that may not have fileno method.'''
675 675 try:
676 676 return os.fstat(fp.fileno())
677 677 except AttributeError:
678 678 return os.stat(fp.name)
679 679
680 680 posixfile = file
681 681
682 682 def is_win_9x():
683 683 '''return true if run on windows 95, 98 or me.'''
684 684 try:
685 685 return sys.getwindowsversion()[3] == 1
686 686 except AttributeError:
687 687 return os.name == 'nt' and 'command' in os.environ.get('comspec', '')
688 688
689 689 getuser_fallback = None
690 690
691 691 def getuser():
692 692 '''return name of current user'''
693 693 try:
694 694 return getpass.getuser()
695 695 except ImportError:
696 696 # import of pwd will fail on windows - try fallback
697 697 if getuser_fallback:
698 698 return getuser_fallback()
699 699 # raised if win32api not available
700 700 raise Abort(_('user name not available - set USERNAME '
701 701 'environment variable'))
702 702
703 703 def username(uid=None):
704 704 """Return the name of the user with the given uid.
705 705
706 706 If uid is None, return the name of the current user."""
707 707 try:
708 708 import pwd
709 709 if uid is None:
710 710 uid = os.getuid()
711 711 try:
712 712 return pwd.getpwuid(uid)[0]
713 713 except KeyError:
714 714 return str(uid)
715 715 except ImportError:
716 716 return None
717 717
718 718 def groupname(gid=None):
719 719 """Return the name of the group with the given gid.
720 720
721 721 If gid is None, return the name of the current group."""
722 722 try:
723 723 import grp
724 724 if gid is None:
725 725 gid = os.getgid()
726 726 try:
727 727 return grp.getgrgid(gid)[0]
728 728 except KeyError:
729 729 return str(gid)
730 730 except ImportError:
731 731 return None
732 732
733 733 # File system features
734 734
735 735 def checkfolding(path):
736 736 """
737 737 Check whether the given path is on a case-sensitive filesystem
738 738
739 739 Requires a path (like /foo/.hg) ending with a foldable final
740 740 directory component.
741 741 """
742 742 s1 = os.stat(path)
743 743 d, b = os.path.split(path)
744 744 p2 = os.path.join(d, b.upper())
745 745 if path == p2:
746 746 p2 = os.path.join(d, b.lower())
747 747 try:
748 748 s2 = os.stat(p2)
749 749 if s2 == s1:
750 750 return False
751 751 return True
752 752 except:
753 753 return True
754 754
755 755 def checkexec(path):
756 756 """
757 757 Check whether the given path is on a filesystem with UNIX-like exec flags
758 758
759 759 Requires a directory (like /foo/.hg)
760 760 """
761 761 fh, fn = tempfile.mkstemp("", "", path)
762 762 os.close(fh)
763 763 m = os.stat(fn).st_mode
764 764 os.chmod(fn, m ^ 0111)
765 765 r = (os.stat(fn).st_mode != m)
766 766 os.unlink(fn)
767 767 return r
768 768
769 769 def execfunc(path, fallback):
770 770 '''return an is_exec() function with default to fallback'''
771 771 if checkexec(path):
772 772 return lambda x: is_exec(os.path.join(path, x))
773 773 return fallback
774 774
775 775 def checklink(path):
776 776 """check whether the given path is on a symlink-capable filesystem"""
777 777 # mktemp is not racy because symlink creation will fail if the
778 778 # file already exists
779 779 name = tempfile.mktemp(dir=path)
780 780 try:
781 781 os.symlink(".", name)
782 782 os.unlink(name)
783 783 return True
784 784 except (OSError, AttributeError):
785 785 return False
786 786
787 787 def linkfunc(path, fallback):
788 788 '''return an is_link() function with default to fallback'''
789 789 if checklink(path):
790 790 return lambda x: os.path.islink(os.path.join(path, x))
791 791 return fallback
792 792
793 793 _umask = os.umask(0)
794 794 os.umask(_umask)
795 795
796 def needbinarypatch():
797 """return True if patches should be applied in binary mode by default."""
798 return os.name == 'nt'
799
796 800 # Platform specific variants
797 801 if os.name == 'nt':
798 802 import msvcrt
799 803 nulldev = 'NUL:'
800 804
801 805 class winstdout:
802 806 '''stdout on windows misbehaves if sent through a pipe'''
803 807
804 808 def __init__(self, fp):
805 809 self.fp = fp
806 810
807 811 def __getattr__(self, key):
808 812 return getattr(self.fp, key)
809 813
810 814 def close(self):
811 815 try:
812 816 self.fp.close()
813 817 except: pass
814 818
815 819 def write(self, s):
816 820 try:
817 821 return self.fp.write(s)
818 822 except IOError, inst:
819 823 if inst.errno != 0: raise
820 824 self.close()
821 825 raise IOError(errno.EPIPE, 'Broken pipe')
822 826
823 827 def flush(self):
824 828 try:
825 829 return self.fp.flush()
826 830 except IOError, inst:
827 831 if inst.errno != errno.EINVAL: raise
828 832 self.close()
829 833 raise IOError(errno.EPIPE, 'Broken pipe')
830 834
831 835 sys.stdout = winstdout(sys.stdout)
832 836
833 837 def system_rcpath():
834 838 try:
835 839 return system_rcpath_win32()
836 840 except:
837 841 return [r'c:\mercurial\mercurial.ini']
838 842
839 843 def user_rcpath():
840 844 '''return os-specific hgrc search path to the user dir'''
841 845 try:
842 846 userrc = user_rcpath_win32()
843 847 except:
844 848 userrc = os.path.join(os.path.expanduser('~'), 'mercurial.ini')
845 849 path = [userrc]
846 850 userprofile = os.environ.get('USERPROFILE')
847 851 if userprofile:
848 852 path.append(os.path.join(userprofile, 'mercurial.ini'))
849 853 return path
850 854
851 855 def parse_patch_output(output_line):
852 856 """parses the output produced by patch and returns the file name"""
853 857 pf = output_line[14:]
854 858 if pf[0] == '`':
855 859 pf = pf[1:-1] # Remove the quotes
856 860 return pf
857 861
858 862 def testpid(pid):
859 863 '''return False if pid dead, True if running or not known'''
860 864 return True
861 865
862 866 def set_exec(f, mode):
863 867 pass
864 868
865 869 def set_link(f, mode):
866 870 pass
867 871
868 872 def set_binary(fd):
869 873 msvcrt.setmode(fd.fileno(), os.O_BINARY)
870 874
871 875 def pconvert(path):
872 876 return path.replace("\\", "/")
873 877
874 878 def localpath(path):
875 879 return path.replace('/', '\\')
876 880
877 881 def normpath(path):
878 882 return pconvert(os.path.normpath(path))
879 883
880 884 makelock = _makelock_file
881 885 readlock = _readlock_file
882 886
883 887 def samestat(s1, s2):
884 888 return False
885 889
886 890 # A sequence of backslashes is special iff it precedes a double quote:
887 891 # - if there's an even number of backslashes, the double quote is not
888 892 # quoted (i.e. it ends the quoted region)
889 893 # - if there's an odd number of backslashes, the double quote is quoted
890 894 # - in both cases, every pair of backslashes is unquoted into a single
891 895 # backslash
892 896 # (See http://msdn2.microsoft.com/en-us/library/a1y7w461.aspx )
893 897 # So, to quote a string, we must surround it in double quotes, double
894 898 # the number of backslashes that preceed double quotes and add another
895 899 # backslash before every double quote (being careful with the double
896 900 # quote we've appended to the end)
897 901 _quotere = None
898 902 def shellquote(s):
899 903 global _quotere
900 904 if _quotere is None:
901 905 _quotere = re.compile(r'(\\*)("|\\$)')
902 906 return '"%s"' % _quotere.sub(r'\1\1\\\2', s)
903 907
904 908 def explain_exit(code):
905 909 return _("exited with status %d") % code, code
906 910
907 911 # if you change this stub into a real check, please try to implement the
908 912 # username and groupname functions above, too.
909 913 def isowner(fp, st=None):
910 914 return True
911 915
912 916 def find_in_path(name, path, default=None):
913 917 '''find name in search path. path can be string (will be split
914 918 with os.pathsep), or iterable thing that returns strings. if name
915 919 found, return path to name. else return default. name is looked up
916 920 using cmd.exe rules, using PATHEXT.'''
917 921 if isinstance(path, str):
918 922 path = path.split(os.pathsep)
919 923
920 924 pathext = os.environ.get('PATHEXT', '.COM;.EXE;.BAT;.CMD')
921 925 pathext = pathext.lower().split(os.pathsep)
922 926 isexec = os.path.splitext(name)[1].lower() in pathext
923 927
924 928 for p in path:
925 929 p_name = os.path.join(p, name)
926 930
927 931 if isexec and os.path.exists(p_name):
928 932 return p_name
929 933
930 934 for ext in pathext:
931 935 p_name_ext = p_name + ext
932 936 if os.path.exists(p_name_ext):
933 937 return p_name_ext
934 938 return default
935 939
936 940 try:
937 941 # override functions with win32 versions if possible
938 942 from util_win32 import *
939 943 if not is_win_9x():
940 944 posixfile = posixfile_nt
941 945 except ImportError:
942 946 pass
943 947
944 948 else:
945 949 nulldev = '/dev/null'
946 950
947 951 def rcfiles(path):
948 952 rcs = [os.path.join(path, 'hgrc')]
949 953 rcdir = os.path.join(path, 'hgrc.d')
950 954 try:
951 955 rcs.extend([os.path.join(rcdir, f) for f in os.listdir(rcdir)
952 956 if f.endswith(".rc")])
953 957 except OSError:
954 958 pass
955 959 return rcs
956 960
957 961 def system_rcpath():
958 962 path = []
959 963 # old mod_python does not set sys.argv
960 964 if len(getattr(sys, 'argv', [])) > 0:
961 965 path.extend(rcfiles(os.path.dirname(sys.argv[0]) +
962 966 '/../etc/mercurial'))
963 967 path.extend(rcfiles('/etc/mercurial'))
964 968 return path
965 969
966 970 def user_rcpath():
967 971 return [os.path.expanduser('~/.hgrc')]
968 972
969 973 def parse_patch_output(output_line):
970 974 """parses the output produced by patch and returns the file name"""
971 975 pf = output_line[14:]
972 976 if pf.startswith("'") and pf.endswith("'") and " " in pf:
973 977 pf = pf[1:-1] # Remove the quotes
974 978 return pf
975 979
976 980 def is_exec(f):
977 981 """check whether a file is executable"""
978 982 return (os.lstat(f).st_mode & 0100 != 0)
979 983
980 984 def set_exec(f, mode):
981 985 s = os.lstat(f).st_mode
982 986 if (s & 0100 != 0) == mode:
983 987 return
984 988 if mode:
985 989 # Turn on +x for every +r bit when making a file executable
986 990 # and obey umask.
987 991 os.chmod(f, s | (s & 0444) >> 2 & ~_umask)
988 992 else:
989 993 os.chmod(f, s & 0666)
990 994
991 995 def set_link(f, mode):
992 996 """make a file a symbolic link/regular file
993 997
994 998 if a file is changed to a link, its contents become the link data
995 999 if a link is changed to a file, its link data become its contents
996 1000 """
997 1001
998 1002 m = os.path.islink(f)
999 1003 if m == bool(mode):
1000 1004 return
1001 1005
1002 1006 if mode: # switch file to link
1003 1007 data = file(f).read()
1004 1008 os.unlink(f)
1005 1009 os.symlink(data, f)
1006 1010 else:
1007 1011 data = os.readlink(f)
1008 1012 os.unlink(f)
1009 1013 file(f, "w").write(data)
1010 1014
1011 1015 def set_binary(fd):
1012 1016 pass
1013 1017
1014 1018 def pconvert(path):
1015 1019 return path
1016 1020
1017 1021 def localpath(path):
1018 1022 return path
1019 1023
1020 1024 normpath = os.path.normpath
1021 1025 samestat = os.path.samestat
1022 1026
1023 1027 def makelock(info, pathname):
1024 1028 try:
1025 1029 os.symlink(info, pathname)
1026 1030 except OSError, why:
1027 1031 if why.errno == errno.EEXIST:
1028 1032 raise
1029 1033 else:
1030 1034 _makelock_file(info, pathname)
1031 1035
1032 1036 def readlock(pathname):
1033 1037 try:
1034 1038 return os.readlink(pathname)
1035 1039 except OSError, why:
1036 1040 if why.errno == errno.EINVAL:
1037 1041 return _readlock_file(pathname)
1038 1042 else:
1039 1043 raise
1040 1044
1041 1045 def shellquote(s):
1042 1046 return "'%s'" % s.replace("'", "'\\''")
1043 1047
1044 1048 def testpid(pid):
1045 1049 '''return False if pid dead, True if running or not sure'''
1046 1050 try:
1047 1051 os.kill(pid, 0)
1048 1052 return True
1049 1053 except OSError, inst:
1050 1054 return inst.errno != errno.ESRCH
1051 1055
1052 1056 def explain_exit(code):
1053 1057 """return a 2-tuple (desc, code) describing a process's status"""
1054 1058 if os.WIFEXITED(code):
1055 1059 val = os.WEXITSTATUS(code)
1056 1060 return _("exited with status %d") % val, val
1057 1061 elif os.WIFSIGNALED(code):
1058 1062 val = os.WTERMSIG(code)
1059 1063 return _("killed by signal %d") % val, val
1060 1064 elif os.WIFSTOPPED(code):
1061 1065 val = os.WSTOPSIG(code)
1062 1066 return _("stopped by signal %d") % val, val
1063 1067 raise ValueError(_("invalid exit code"))
1064 1068
1065 1069 def isowner(fp, st=None):
1066 1070 """Return True if the file object f belongs to the current user.
1067 1071
1068 1072 The return value of a util.fstat(f) may be passed as the st argument.
1069 1073 """
1070 1074 if st is None:
1071 1075 st = fstat(fp)
1072 1076 return st.st_uid == os.getuid()
1073 1077
1074 1078 def find_in_path(name, path, default=None):
1075 1079 '''find name in search path. path can be string (will be split
1076 1080 with os.pathsep), or iterable thing that returns strings. if name
1077 1081 found, return path to name. else return default.'''
1078 1082 if isinstance(path, str):
1079 1083 path = path.split(os.pathsep)
1080 1084 for p in path:
1081 1085 p_name = os.path.join(p, name)
1082 1086 if os.path.exists(p_name):
1083 1087 return p_name
1084 1088 return default
1085 1089
1086 1090 def _buildencodefun():
1087 1091 e = '_'
1088 1092 win_reserved = [ord(x) for x in '\\:*?"<>|']
1089 1093 cmap = dict([ (chr(x), chr(x)) for x in xrange(127) ])
1090 1094 for x in (range(32) + range(126, 256) + win_reserved):
1091 1095 cmap[chr(x)] = "~%02x" % x
1092 1096 for x in range(ord("A"), ord("Z")+1) + [ord(e)]:
1093 1097 cmap[chr(x)] = e + chr(x).lower()
1094 1098 dmap = {}
1095 1099 for k, v in cmap.iteritems():
1096 1100 dmap[v] = k
1097 1101 def decode(s):
1098 1102 i = 0
1099 1103 while i < len(s):
1100 1104 for l in xrange(1, 4):
1101 1105 try:
1102 1106 yield dmap[s[i:i+l]]
1103 1107 i += l
1104 1108 break
1105 1109 except KeyError:
1106 1110 pass
1107 1111 else:
1108 1112 raise KeyError
1109 1113 return (lambda s: "".join([cmap[c] for c in s]),
1110 1114 lambda s: "".join(list(decode(s))))
1111 1115
1112 1116 encodefilename, decodefilename = _buildencodefun()
1113 1117
1114 1118 def encodedopener(openerfn, fn):
1115 1119 def o(path, *args, **kw):
1116 1120 return openerfn(fn(path), *args, **kw)
1117 1121 return o
1118 1122
1119 1123 def opener(base, audit=True):
1120 1124 """
1121 1125 return a function that opens files relative to base
1122 1126
1123 1127 this function is used to hide the details of COW semantics and
1124 1128 remote file access from higher level code.
1125 1129 """
1126 1130 p = base
1127 1131 audit_p = audit
1128 1132
1129 1133 def mktempcopy(name, emptyok=False):
1130 1134 d, fn = os.path.split(name)
1131 1135 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1132 1136 os.close(fd)
1133 1137 # Temporary files are created with mode 0600, which is usually not
1134 1138 # what we want. If the original file already exists, just copy
1135 1139 # its mode. Otherwise, manually obey umask.
1136 1140 try:
1137 1141 st_mode = os.lstat(name).st_mode
1138 1142 except OSError, inst:
1139 1143 if inst.errno != errno.ENOENT:
1140 1144 raise
1141 1145 st_mode = 0666 & ~_umask
1142 1146 os.chmod(temp, st_mode)
1143 1147 if emptyok:
1144 1148 return temp
1145 1149 try:
1146 1150 try:
1147 1151 ifp = posixfile(name, "rb")
1148 1152 except IOError, inst:
1149 1153 if inst.errno == errno.ENOENT:
1150 1154 return temp
1151 1155 if not getattr(inst, 'filename', None):
1152 1156 inst.filename = name
1153 1157 raise
1154 1158 ofp = posixfile(temp, "wb")
1155 1159 for chunk in filechunkiter(ifp):
1156 1160 ofp.write(chunk)
1157 1161 ifp.close()
1158 1162 ofp.close()
1159 1163 except:
1160 1164 try: os.unlink(temp)
1161 1165 except: pass
1162 1166 raise
1163 1167 return temp
1164 1168
1165 1169 class atomictempfile(posixfile):
1166 1170 """the file will only be copied when rename is called"""
1167 1171 def __init__(self, name, mode):
1168 1172 self.__name = name
1169 1173 self.temp = mktempcopy(name, emptyok=('w' in mode))
1170 1174 posixfile.__init__(self, self.temp, mode)
1171 1175 def rename(self):
1172 1176 if not self.closed:
1173 1177 posixfile.close(self)
1174 1178 rename(self.temp, localpath(self.__name))
1175 1179 def __del__(self):
1176 1180 if not self.closed:
1177 1181 try:
1178 1182 os.unlink(self.temp)
1179 1183 except: pass
1180 1184 posixfile.close(self)
1181 1185
1182 1186 class atomicfile(atomictempfile):
1183 1187 """the file will only be copied on close"""
1184 1188 def __init__(self, name, mode):
1185 1189 self._err = False
1186 1190 atomictempfile.__init__(self, name, mode)
1187 1191 def write(self, s):
1188 1192 try:
1189 1193 atomictempfile.write(self, s)
1190 1194 except:
1191 1195 self._err = True
1192 1196 raise
1193 1197 def close(self):
1194 1198 self.rename()
1195 1199 def __del__(self):
1196 1200 if not self._err:
1197 1201 self.rename()
1198 1202
1199 1203 def o(path, mode="r", text=False, atomic=False, atomictemp=False):
1200 1204 if audit_p:
1201 1205 audit_path(path)
1202 1206 f = os.path.join(p, path)
1203 1207
1204 1208 if not text:
1205 1209 mode += "b" # for that other OS
1206 1210
1207 1211 if mode[0] != "r":
1208 1212 try:
1209 1213 nlink = nlinks(f)
1210 1214 except OSError:
1211 1215 nlink = 0
1212 1216 d = os.path.dirname(f)
1213 1217 if not os.path.isdir(d):
1214 1218 os.makedirs(d)
1215 1219 if atomic:
1216 1220 return atomicfile(f, mode)
1217 1221 elif atomictemp:
1218 1222 return atomictempfile(f, mode)
1219 1223 if nlink > 1:
1220 1224 rename(mktempcopy(f), f)
1221 1225 return posixfile(f, mode)
1222 1226
1223 1227 return o
1224 1228
1225 1229 class chunkbuffer(object):
1226 1230 """Allow arbitrary sized chunks of data to be efficiently read from an
1227 1231 iterator over chunks of arbitrary size."""
1228 1232
1229 1233 def __init__(self, in_iter, targetsize = 2**16):
1230 1234 """in_iter is the iterator that's iterating over the input chunks.
1231 1235 targetsize is how big a buffer to try to maintain."""
1232 1236 self.in_iter = iter(in_iter)
1233 1237 self.buf = ''
1234 1238 self.targetsize = int(targetsize)
1235 1239 if self.targetsize <= 0:
1236 1240 raise ValueError(_("targetsize must be greater than 0, was %d") %
1237 1241 targetsize)
1238 1242 self.iterempty = False
1239 1243
1240 1244 def fillbuf(self):
1241 1245 """Ignore target size; read every chunk from iterator until empty."""
1242 1246 if not self.iterempty:
1243 1247 collector = cStringIO.StringIO()
1244 1248 collector.write(self.buf)
1245 1249 for ch in self.in_iter:
1246 1250 collector.write(ch)
1247 1251 self.buf = collector.getvalue()
1248 1252 self.iterempty = True
1249 1253
1250 1254 def read(self, l):
1251 1255 """Read L bytes of data from the iterator of chunks of data.
1252 1256 Returns less than L bytes if the iterator runs dry."""
1253 1257 if l > len(self.buf) and not self.iterempty:
1254 1258 # Clamp to a multiple of self.targetsize
1255 1259 targetsize = self.targetsize * ((l // self.targetsize) + 1)
1256 1260 collector = cStringIO.StringIO()
1257 1261 collector.write(self.buf)
1258 1262 collected = len(self.buf)
1259 1263 for chunk in self.in_iter:
1260 1264 collector.write(chunk)
1261 1265 collected += len(chunk)
1262 1266 if collected >= targetsize:
1263 1267 break
1264 1268 if collected < targetsize:
1265 1269 self.iterempty = True
1266 1270 self.buf = collector.getvalue()
1267 1271 s, self.buf = self.buf[:l], buffer(self.buf, l)
1268 1272 return s
1269 1273
1270 1274 def filechunkiter(f, size=65536, limit=None):
1271 1275 """Create a generator that produces the data in the file size
1272 1276 (default 65536) bytes at a time, up to optional limit (default is
1273 1277 to read all data). Chunks may be less than size bytes if the
1274 1278 chunk is the last chunk in the file, or the file is a socket or
1275 1279 some other type of file that sometimes reads less data than is
1276 1280 requested."""
1277 1281 assert size >= 0
1278 1282 assert limit is None or limit >= 0
1279 1283 while True:
1280 1284 if limit is None: nbytes = size
1281 1285 else: nbytes = min(limit, size)
1282 1286 s = nbytes and f.read(nbytes)
1283 1287 if not s: break
1284 1288 if limit: limit -= len(s)
1285 1289 yield s
1286 1290
1287 1291 def makedate():
1288 1292 lt = time.localtime()
1289 1293 if lt[8] == 1 and time.daylight:
1290 1294 tz = time.altzone
1291 1295 else:
1292 1296 tz = time.timezone
1293 1297 return time.mktime(lt), tz
1294 1298
1295 1299 def datestr(date=None, format='%a %b %d %H:%M:%S %Y', timezone=True):
1296 1300 """represent a (unixtime, offset) tuple as a localized time.
1297 1301 unixtime is seconds since the epoch, and offset is the time zone's
1298 1302 number of seconds away from UTC. if timezone is false, do not
1299 1303 append time zone to string."""
1300 1304 t, tz = date or makedate()
1301 1305 s = time.strftime(format, time.gmtime(float(t) - tz))
1302 1306 if timezone:
1303 1307 s += " %+03d%02d" % (-tz / 3600, ((-tz % 3600) / 60))
1304 1308 return s
1305 1309
1306 1310 def strdate(string, format, defaults):
1307 1311 """parse a localized time string and return a (unixtime, offset) tuple.
1308 1312 if the string cannot be parsed, ValueError is raised."""
1309 1313 def timezone(string):
1310 1314 tz = string.split()[-1]
1311 1315 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1312 1316 tz = int(tz)
1313 1317 offset = - 3600 * (tz / 100) - 60 * (tz % 100)
1314 1318 return offset
1315 1319 if tz == "GMT" or tz == "UTC":
1316 1320 return 0
1317 1321 return None
1318 1322
1319 1323 # NOTE: unixtime = localunixtime + offset
1320 1324 offset, date = timezone(string), string
1321 1325 if offset != None:
1322 1326 date = " ".join(string.split()[:-1])
1323 1327
1324 1328 # add missing elements from defaults
1325 1329 for part in defaults:
1326 1330 found = [True for p in part if ("%"+p) in format]
1327 1331 if not found:
1328 1332 date += "@" + defaults[part]
1329 1333 format += "@%" + part[0]
1330 1334
1331 1335 timetuple = time.strptime(date, format)
1332 1336 localunixtime = int(calendar.timegm(timetuple))
1333 1337 if offset is None:
1334 1338 # local timezone
1335 1339 unixtime = int(time.mktime(timetuple))
1336 1340 offset = unixtime - localunixtime
1337 1341 else:
1338 1342 unixtime = localunixtime + offset
1339 1343 return unixtime, offset
1340 1344
1341 1345 def parsedate(string, formats=None, defaults=None):
1342 1346 """parse a localized time string and return a (unixtime, offset) tuple.
1343 1347 The date may be a "unixtime offset" string or in one of the specified
1344 1348 formats."""
1345 1349 if not string:
1346 1350 return 0, 0
1347 1351 if not formats:
1348 1352 formats = defaultdateformats
1349 1353 string = string.strip()
1350 1354 try:
1351 1355 when, offset = map(int, string.split(' '))
1352 1356 except ValueError:
1353 1357 # fill out defaults
1354 1358 if not defaults:
1355 1359 defaults = {}
1356 1360 now = makedate()
1357 1361 for part in "d mb yY HI M S".split():
1358 1362 if part not in defaults:
1359 1363 if part[0] in "HMS":
1360 1364 defaults[part] = "00"
1361 1365 elif part[0] in "dm":
1362 1366 defaults[part] = "1"
1363 1367 else:
1364 1368 defaults[part] = datestr(now, "%" + part[0], False)
1365 1369
1366 1370 for format in formats:
1367 1371 try:
1368 1372 when, offset = strdate(string, format, defaults)
1369 1373 except ValueError:
1370 1374 pass
1371 1375 else:
1372 1376 break
1373 1377 else:
1374 1378 raise Abort(_('invalid date: %r ') % string)
1375 1379 # validate explicit (probably user-specified) date and
1376 1380 # time zone offset. values must fit in signed 32 bits for
1377 1381 # current 32-bit linux runtimes. timezones go from UTC-12
1378 1382 # to UTC+14
1379 1383 if abs(when) > 0x7fffffff:
1380 1384 raise Abort(_('date exceeds 32 bits: %d') % when)
1381 1385 if offset < -50400 or offset > 43200:
1382 1386 raise Abort(_('impossible time zone offset: %d') % offset)
1383 1387 return when, offset
1384 1388
1385 1389 def matchdate(date):
1386 1390 """Return a function that matches a given date match specifier
1387 1391
1388 1392 Formats include:
1389 1393
1390 1394 '{date}' match a given date to the accuracy provided
1391 1395
1392 1396 '<{date}' on or before a given date
1393 1397
1394 1398 '>{date}' on or after a given date
1395 1399
1396 1400 """
1397 1401
1398 1402 def lower(date):
1399 1403 return parsedate(date, extendeddateformats)[0]
1400 1404
1401 1405 def upper(date):
1402 1406 d = dict(mb="12", HI="23", M="59", S="59")
1403 1407 for days in "31 30 29".split():
1404 1408 try:
1405 1409 d["d"] = days
1406 1410 return parsedate(date, extendeddateformats, d)[0]
1407 1411 except:
1408 1412 pass
1409 1413 d["d"] = "28"
1410 1414 return parsedate(date, extendeddateformats, d)[0]
1411 1415
1412 1416 if date[0] == "<":
1413 1417 when = upper(date[1:])
1414 1418 return lambda x: x <= when
1415 1419 elif date[0] == ">":
1416 1420 when = lower(date[1:])
1417 1421 return lambda x: x >= when
1418 1422 elif date[0] == "-":
1419 1423 try:
1420 1424 days = int(date[1:])
1421 1425 except ValueError:
1422 1426 raise Abort(_("invalid day spec: %s") % date[1:])
1423 1427 when = makedate()[0] - days * 3600 * 24
1424 1428 return lambda x: x >= when
1425 1429 elif " to " in date:
1426 1430 a, b = date.split(" to ")
1427 1431 start, stop = lower(a), upper(b)
1428 1432 return lambda x: x >= start and x <= stop
1429 1433 else:
1430 1434 start, stop = lower(date), upper(date)
1431 1435 return lambda x: x >= start and x <= stop
1432 1436
1433 1437 def shortuser(user):
1434 1438 """Return a short representation of a user name or email address."""
1435 1439 f = user.find('@')
1436 1440 if f >= 0:
1437 1441 user = user[:f]
1438 1442 f = user.find('<')
1439 1443 if f >= 0:
1440 1444 user = user[f+1:]
1441 1445 f = user.find(' ')
1442 1446 if f >= 0:
1443 1447 user = user[:f]
1444 1448 f = user.find('.')
1445 1449 if f >= 0:
1446 1450 user = user[:f]
1447 1451 return user
1448 1452
1449 1453 def ellipsis(text, maxlength=400):
1450 1454 """Trim string to at most maxlength (default: 400) characters."""
1451 1455 if len(text) <= maxlength:
1452 1456 return text
1453 1457 else:
1454 1458 return "%s..." % (text[:maxlength-3])
1455 1459
1456 1460 def walkrepos(path):
1457 1461 '''yield every hg repository under path, recursively.'''
1458 1462 def errhandler(err):
1459 1463 if err.filename == path:
1460 1464 raise err
1461 1465
1462 1466 for root, dirs, files in os.walk(path, onerror=errhandler):
1463 1467 for d in dirs:
1464 1468 if d == '.hg':
1465 1469 yield root
1466 1470 dirs[:] = []
1467 1471 break
1468 1472
1469 1473 _rcpath = None
1470 1474
1471 1475 def os_rcpath():
1472 1476 '''return default os-specific hgrc search path'''
1473 1477 path = system_rcpath()
1474 1478 path.extend(user_rcpath())
1475 1479 path = [os.path.normpath(f) for f in path]
1476 1480 return path
1477 1481
1478 1482 def rcpath():
1479 1483 '''return hgrc search path. if env var HGRCPATH is set, use it.
1480 1484 for each item in path, if directory, use files ending in .rc,
1481 1485 else use item.
1482 1486 make HGRCPATH empty to only look in .hg/hgrc of current repo.
1483 1487 if no HGRCPATH, use default os-specific path.'''
1484 1488 global _rcpath
1485 1489 if _rcpath is None:
1486 1490 if 'HGRCPATH' in os.environ:
1487 1491 _rcpath = []
1488 1492 for p in os.environ['HGRCPATH'].split(os.pathsep):
1489 1493 if not p: continue
1490 1494 if os.path.isdir(p):
1491 1495 for f in os.listdir(p):
1492 1496 if f.endswith('.rc'):
1493 1497 _rcpath.append(os.path.join(p, f))
1494 1498 else:
1495 1499 _rcpath.append(p)
1496 1500 else:
1497 1501 _rcpath = os_rcpath()
1498 1502 return _rcpath
1499 1503
1500 1504 def bytecount(nbytes):
1501 1505 '''return byte count formatted as readable string, with units'''
1502 1506
1503 1507 units = (
1504 1508 (100, 1<<30, _('%.0f GB')),
1505 1509 (10, 1<<30, _('%.1f GB')),
1506 1510 (1, 1<<30, _('%.2f GB')),
1507 1511 (100, 1<<20, _('%.0f MB')),
1508 1512 (10, 1<<20, _('%.1f MB')),
1509 1513 (1, 1<<20, _('%.2f MB')),
1510 1514 (100, 1<<10, _('%.0f KB')),
1511 1515 (10, 1<<10, _('%.1f KB')),
1512 1516 (1, 1<<10, _('%.2f KB')),
1513 1517 (1, 1, _('%.0f bytes')),
1514 1518 )
1515 1519
1516 1520 for multiplier, divisor, format in units:
1517 1521 if nbytes >= divisor * multiplier:
1518 1522 return format % (nbytes / float(divisor))
1519 1523 return units[-1][2] % nbytes
1520 1524
1521 1525 def drop_scheme(scheme, path):
1522 1526 sc = scheme + ':'
1523 1527 if path.startswith(sc):
1524 1528 path = path[len(sc):]
1525 1529 if path.startswith('//'):
1526 1530 path = path[2:]
1527 1531 return path
General Comments 0
You need to be logged in to leave comments. Login now