##// END OF EJS Templates
merge with stable
Matt Mackall -
r15089:bfe903b1 merge default
parent child Browse files
Show More
@@ -1,1859 +1,1859
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 import cStringIO, email.Parser, os, errno, re
10 10 import tempfile, zlib, shutil
11 11
12 12 from i18n import _
13 13 from node import hex, nullid, short
14 14 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
15 15 import context
16 16
17 17 gitre = re.compile('diff --git a/(.*) b/(.*)')
18 18
19 19 class PatchError(Exception):
20 20 pass
21 21
22 22
23 23 # public functions
24 24
25 25 def split(stream):
26 26 '''return an iterator of individual patches from a stream'''
27 27 def isheader(line, inheader):
28 28 if inheader and line[0] in (' ', '\t'):
29 29 # continuation
30 30 return True
31 31 if line[0] in (' ', '-', '+'):
32 32 # diff line - don't check for header pattern in there
33 33 return False
34 34 l = line.split(': ', 1)
35 35 return len(l) == 2 and ' ' not in l[0]
36 36
37 37 def chunk(lines):
38 38 return cStringIO.StringIO(''.join(lines))
39 39
40 40 def hgsplit(stream, cur):
41 41 inheader = True
42 42
43 43 for line in stream:
44 44 if not line.strip():
45 45 inheader = False
46 46 if not inheader and line.startswith('# HG changeset patch'):
47 47 yield chunk(cur)
48 48 cur = []
49 49 inheader = True
50 50
51 51 cur.append(line)
52 52
53 53 if cur:
54 54 yield chunk(cur)
55 55
56 56 def mboxsplit(stream, cur):
57 57 for line in stream:
58 58 if line.startswith('From '):
59 59 for c in split(chunk(cur[1:])):
60 60 yield c
61 61 cur = []
62 62
63 63 cur.append(line)
64 64
65 65 if cur:
66 66 for c in split(chunk(cur[1:])):
67 67 yield c
68 68
69 69 def mimesplit(stream, cur):
70 70 def msgfp(m):
71 71 fp = cStringIO.StringIO()
72 72 g = email.Generator.Generator(fp, mangle_from_=False)
73 73 g.flatten(m)
74 74 fp.seek(0)
75 75 return fp
76 76
77 77 for line in stream:
78 78 cur.append(line)
79 79 c = chunk(cur)
80 80
81 81 m = email.Parser.Parser().parse(c)
82 82 if not m.is_multipart():
83 83 yield msgfp(m)
84 84 else:
85 85 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
86 86 for part in m.walk():
87 87 ct = part.get_content_type()
88 88 if ct not in ok_types:
89 89 continue
90 90 yield msgfp(part)
91 91
92 92 def headersplit(stream, cur):
93 93 inheader = False
94 94
95 95 for line in stream:
96 96 if not inheader and isheader(line, inheader):
97 97 yield chunk(cur)
98 98 cur = []
99 99 inheader = True
100 100 if inheader and not isheader(line, inheader):
101 101 inheader = False
102 102
103 103 cur.append(line)
104 104
105 105 if cur:
106 106 yield chunk(cur)
107 107
108 108 def remainder(cur):
109 109 yield chunk(cur)
110 110
111 111 class fiter(object):
112 112 def __init__(self, fp):
113 113 self.fp = fp
114 114
115 115 def __iter__(self):
116 116 return self
117 117
118 118 def next(self):
119 119 l = self.fp.readline()
120 120 if not l:
121 121 raise StopIteration
122 122 return l
123 123
124 124 inheader = False
125 125 cur = []
126 126
127 127 mimeheaders = ['content-type']
128 128
129 129 if not util.safehasattr(stream, 'next'):
130 130 # http responses, for example, have readline but not next
131 131 stream = fiter(stream)
132 132
133 133 for line in stream:
134 134 cur.append(line)
135 135 if line.startswith('# HG changeset patch'):
136 136 return hgsplit(stream, cur)
137 137 elif line.startswith('From '):
138 138 return mboxsplit(stream, cur)
139 139 elif isheader(line, inheader):
140 140 inheader = True
141 141 if line.split(':', 1)[0].lower() in mimeheaders:
142 142 # let email parser handle this
143 143 return mimesplit(stream, cur)
144 144 elif line.startswith('--- ') and inheader:
145 145 # No evil headers seen by diff start, split by hand
146 146 return headersplit(stream, cur)
147 147 # Not enough info, keep reading
148 148
149 149 # if we are here, we have a very plain patch
150 150 return remainder(cur)
151 151
152 152 def extract(ui, fileobj):
153 153 '''extract patch from data read from fileobj.
154 154
155 155 patch can be a normal patch or contained in an email message.
156 156
157 157 return tuple (filename, message, user, date, branch, node, p1, p2).
158 158 Any item in the returned tuple can be None. If filename is None,
159 159 fileobj did not contain a patch. Caller must unlink filename when done.'''
160 160
161 161 # attempt to detect the start of a patch
162 162 # (this heuristic is borrowed from quilt)
163 163 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
164 164 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
165 165 r'---[ \t].*?^\+\+\+[ \t]|'
166 166 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
167 167
168 168 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
169 169 tmpfp = os.fdopen(fd, 'w')
170 170 try:
171 171 msg = email.Parser.Parser().parse(fileobj)
172 172
173 173 subject = msg['Subject']
174 174 user = msg['From']
175 175 if not subject and not user:
176 176 # Not an email, restore parsed headers if any
177 177 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
178 178
179 179 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
180 180 # should try to parse msg['Date']
181 181 date = None
182 182 nodeid = None
183 183 branch = None
184 184 parents = []
185 185
186 186 if subject:
187 187 if subject.startswith('[PATCH'):
188 188 pend = subject.find(']')
189 189 if pend >= 0:
190 190 subject = subject[pend + 1:].lstrip()
191 191 subject = subject.replace('\n\t', ' ')
192 192 ui.debug('Subject: %s\n' % subject)
193 193 if user:
194 194 ui.debug('From: %s\n' % user)
195 195 diffs_seen = 0
196 196 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
197 197 message = ''
198 198 for part in msg.walk():
199 199 content_type = part.get_content_type()
200 200 ui.debug('Content-Type: %s\n' % content_type)
201 201 if content_type not in ok_types:
202 202 continue
203 203 payload = part.get_payload(decode=True)
204 204 m = diffre.search(payload)
205 205 if m:
206 206 hgpatch = False
207 207 hgpatchheader = False
208 208 ignoretext = False
209 209
210 210 ui.debug('found patch at byte %d\n' % m.start(0))
211 211 diffs_seen += 1
212 212 cfp = cStringIO.StringIO()
213 213 for line in payload[:m.start(0)].splitlines():
214 214 if line.startswith('# HG changeset patch') and not hgpatch:
215 215 ui.debug('patch generated by hg export\n')
216 216 hgpatch = True
217 217 hgpatchheader = True
218 218 # drop earlier commit message content
219 219 cfp.seek(0)
220 220 cfp.truncate()
221 221 subject = None
222 222 elif hgpatchheader:
223 223 if line.startswith('# User '):
224 224 user = line[7:]
225 225 ui.debug('From: %s\n' % user)
226 226 elif line.startswith("# Date "):
227 227 date = line[7:]
228 228 elif line.startswith("# Branch "):
229 229 branch = line[9:]
230 230 elif line.startswith("# Node ID "):
231 231 nodeid = line[10:]
232 232 elif line.startswith("# Parent "):
233 233 parents.append(line[10:])
234 234 elif not line.startswith("# "):
235 235 hgpatchheader = False
236 236 elif line == '---' and gitsendmail:
237 237 ignoretext = True
238 238 if not hgpatchheader and not ignoretext:
239 239 cfp.write(line)
240 240 cfp.write('\n')
241 241 message = cfp.getvalue()
242 242 if tmpfp:
243 243 tmpfp.write(payload)
244 244 if not payload.endswith('\n'):
245 245 tmpfp.write('\n')
246 246 elif not diffs_seen and message and content_type == 'text/plain':
247 247 message += '\n' + payload
248 248 except:
249 249 tmpfp.close()
250 250 os.unlink(tmpname)
251 251 raise
252 252
253 253 if subject and not message.startswith(subject):
254 254 message = '%s\n%s' % (subject, message)
255 255 tmpfp.close()
256 256 if not diffs_seen:
257 257 os.unlink(tmpname)
258 258 return None, message, user, date, branch, None, None, None
259 259 p1 = parents and parents.pop(0) or None
260 260 p2 = parents and parents.pop(0) or None
261 261 return tmpname, message, user, date, branch, nodeid, p1, p2
262 262
263 263 class patchmeta(object):
264 264 """Patched file metadata
265 265
266 266 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
267 267 or COPY. 'path' is patched file path. 'oldpath' is set to the
268 268 origin file when 'op' is either COPY or RENAME, None otherwise. If
269 269 file mode is changed, 'mode' is a tuple (islink, isexec) where
270 270 'islink' is True if the file is a symlink and 'isexec' is True if
271 271 the file is executable. Otherwise, 'mode' is None.
272 272 """
273 273 def __init__(self, path):
274 274 self.path = path
275 275 self.oldpath = None
276 276 self.mode = None
277 277 self.op = 'MODIFY'
278 278 self.binary = False
279 279
280 280 def setmode(self, mode):
281 281 islink = mode & 020000
282 282 isexec = mode & 0100
283 283 self.mode = (islink, isexec)
284 284
285 285 def copy(self):
286 286 other = patchmeta(self.path)
287 287 other.oldpath = self.oldpath
288 288 other.mode = self.mode
289 289 other.op = self.op
290 290 other.binary = self.binary
291 291 return other
292 292
293 293 def __repr__(self):
294 294 return "<patchmeta %s %r>" % (self.op, self.path)
295 295
296 296 def readgitpatch(lr):
297 297 """extract git-style metadata about patches from <patchname>"""
298 298
299 299 # Filter patch for git information
300 300 gp = None
301 301 gitpatches = []
302 302 for line in lr:
303 303 line = line.rstrip(' \r\n')
304 304 if line.startswith('diff --git'):
305 305 m = gitre.match(line)
306 306 if m:
307 307 if gp:
308 308 gitpatches.append(gp)
309 309 dst = m.group(2)
310 310 gp = patchmeta(dst)
311 311 elif gp:
312 312 if line.startswith('--- '):
313 313 gitpatches.append(gp)
314 314 gp = None
315 315 continue
316 316 if line.startswith('rename from '):
317 317 gp.op = 'RENAME'
318 318 gp.oldpath = line[12:]
319 319 elif line.startswith('rename to '):
320 320 gp.path = line[10:]
321 321 elif line.startswith('copy from '):
322 322 gp.op = 'COPY'
323 323 gp.oldpath = line[10:]
324 324 elif line.startswith('copy to '):
325 325 gp.path = line[8:]
326 326 elif line.startswith('deleted file'):
327 327 gp.op = 'DELETE'
328 328 elif line.startswith('new file mode '):
329 329 gp.op = 'ADD'
330 330 gp.setmode(int(line[-6:], 8))
331 331 elif line.startswith('new mode '):
332 332 gp.setmode(int(line[-6:], 8))
333 333 elif line.startswith('GIT binary patch'):
334 334 gp.binary = True
335 335 if gp:
336 336 gitpatches.append(gp)
337 337
338 338 return gitpatches
339 339
340 340 class linereader(object):
341 341 # simple class to allow pushing lines back into the input stream
342 342 def __init__(self, fp):
343 343 self.fp = fp
344 344 self.buf = []
345 345
346 346 def push(self, line):
347 347 if line is not None:
348 348 self.buf.append(line)
349 349
350 350 def readline(self):
351 351 if self.buf:
352 352 l = self.buf[0]
353 353 del self.buf[0]
354 354 return l
355 355 return self.fp.readline()
356 356
357 357 def __iter__(self):
358 358 while True:
359 359 l = self.readline()
360 360 if not l:
361 361 break
362 362 yield l
363 363
364 364 class abstractbackend(object):
365 365 def __init__(self, ui):
366 366 self.ui = ui
367 367
368 368 def getfile(self, fname):
369 369 """Return target file data and flags as a (data, (islink,
370 370 isexec)) tuple.
371 371 """
372 372 raise NotImplementedError
373 373
374 374 def setfile(self, fname, data, mode, copysource):
375 375 """Write data to target file fname and set its mode. mode is a
376 376 (islink, isexec) tuple. If data is None, the file content should
377 377 be left unchanged. If the file is modified after being copied,
378 378 copysource is set to the original file name.
379 379 """
380 380 raise NotImplementedError
381 381
382 382 def unlink(self, fname):
383 383 """Unlink target file."""
384 384 raise NotImplementedError
385 385
386 386 def writerej(self, fname, failed, total, lines):
387 387 """Write rejected lines for fname. total is the number of hunks
388 388 which failed to apply and total the total number of hunks for this
389 389 files.
390 390 """
391 391 pass
392 392
393 393 def exists(self, fname):
394 394 raise NotImplementedError
395 395
396 396 class fsbackend(abstractbackend):
397 397 def __init__(self, ui, basedir):
398 398 super(fsbackend, self).__init__(ui)
399 399 self.opener = scmutil.opener(basedir)
400 400
401 401 def _join(self, f):
402 402 return os.path.join(self.opener.base, f)
403 403
404 404 def getfile(self, fname):
405 405 path = self._join(fname)
406 406 if os.path.islink(path):
407 407 return (os.readlink(path), (True, False))
408 408 isexec = False
409 409 try:
410 410 isexec = os.lstat(path).st_mode & 0100 != 0
411 411 except OSError, e:
412 412 if e.errno != errno.ENOENT:
413 413 raise
414 414 return (self.opener.read(fname), (False, isexec))
415 415
416 416 def setfile(self, fname, data, mode, copysource):
417 417 islink, isexec = mode
418 418 if data is None:
419 419 util.setflags(self._join(fname), islink, isexec)
420 420 return
421 421 if islink:
422 422 self.opener.symlink(data, fname)
423 423 else:
424 424 self.opener.write(fname, data)
425 425 if isexec:
426 426 util.setflags(self._join(fname), False, True)
427 427
428 428 def unlink(self, fname):
429 429 try:
430 430 util.unlinkpath(self._join(fname))
431 431 except OSError, inst:
432 432 if inst.errno != errno.ENOENT:
433 433 raise
434 434
435 435 def writerej(self, fname, failed, total, lines):
436 436 fname = fname + ".rej"
437 437 self.ui.warn(
438 438 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
439 439 (failed, total, fname))
440 440 fp = self.opener(fname, 'w')
441 441 fp.writelines(lines)
442 442 fp.close()
443 443
444 444 def exists(self, fname):
445 445 return os.path.lexists(self._join(fname))
446 446
447 447 class workingbackend(fsbackend):
448 448 def __init__(self, ui, repo, similarity):
449 449 super(workingbackend, self).__init__(ui, repo.root)
450 450 self.repo = repo
451 451 self.similarity = similarity
452 452 self.removed = set()
453 453 self.changed = set()
454 454 self.copied = []
455 455
456 456 def _checkknown(self, fname):
457 457 if self.repo.dirstate[fname] == '?' and self.exists(fname):
458 458 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
459 459
460 460 def setfile(self, fname, data, mode, copysource):
461 461 self._checkknown(fname)
462 462 super(workingbackend, self).setfile(fname, data, mode, copysource)
463 463 if copysource is not None:
464 464 self.copied.append((copysource, fname))
465 465 self.changed.add(fname)
466 466
467 467 def unlink(self, fname):
468 468 self._checkknown(fname)
469 469 super(workingbackend, self).unlink(fname)
470 470 self.removed.add(fname)
471 471 self.changed.add(fname)
472 472
473 473 def close(self):
474 474 wctx = self.repo[None]
475 475 addremoved = set(self.changed)
476 476 for src, dst in self.copied:
477 477 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
478 478 addremoved.discard(src)
479 479 if (not self.similarity) and self.removed:
480 480 wctx.forget(sorted(self.removed))
481 481 if addremoved:
482 482 cwd = self.repo.getcwd()
483 483 if cwd:
484 484 addremoved = [util.pathto(self.repo.root, cwd, f)
485 485 for f in addremoved]
486 486 scmutil.addremove(self.repo, addremoved, similarity=self.similarity)
487 487 return sorted(self.changed)
488 488
489 489 class filestore(object):
490 490 def __init__(self, maxsize=None):
491 491 self.opener = None
492 492 self.files = {}
493 493 self.created = 0
494 494 self.maxsize = maxsize
495 495 if self.maxsize is None:
496 496 self.maxsize = 4*(2**20)
497 497 self.size = 0
498 498 self.data = {}
499 499
500 500 def setfile(self, fname, data, mode, copied=None):
501 501 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
502 502 self.data[fname] = (data, mode, copied)
503 503 self.size += len(data)
504 504 else:
505 505 if self.opener is None:
506 506 root = tempfile.mkdtemp(prefix='hg-patch-')
507 507 self.opener = scmutil.opener(root)
508 508 # Avoid filename issues with these simple names
509 509 fn = str(self.created)
510 510 self.opener.write(fn, data)
511 511 self.created += 1
512 512 self.files[fname] = (fn, mode, copied)
513 513
514 514 def getfile(self, fname):
515 515 if fname in self.data:
516 516 return self.data[fname]
517 517 if not self.opener or fname not in self.files:
518 518 raise IOError()
519 519 fn, mode, copied = self.files[fname]
520 520 return self.opener.read(fn), mode, copied
521 521
522 522 def close(self):
523 523 if self.opener:
524 524 shutil.rmtree(self.opener.base)
525 525
526 526 class repobackend(abstractbackend):
527 527 def __init__(self, ui, repo, ctx, store):
528 528 super(repobackend, self).__init__(ui)
529 529 self.repo = repo
530 530 self.ctx = ctx
531 531 self.store = store
532 532 self.changed = set()
533 533 self.removed = set()
534 534 self.copied = {}
535 535
536 536 def _checkknown(self, fname):
537 537 if fname not in self.ctx:
538 538 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
539 539
540 540 def getfile(self, fname):
541 541 try:
542 542 fctx = self.ctx[fname]
543 543 except error.LookupError:
544 544 raise IOError()
545 545 flags = fctx.flags()
546 546 return fctx.data(), ('l' in flags, 'x' in flags)
547 547
548 548 def setfile(self, fname, data, mode, copysource):
549 549 if copysource:
550 550 self._checkknown(copysource)
551 551 if data is None:
552 552 data = self.ctx[fname].data()
553 553 self.store.setfile(fname, data, mode, copysource)
554 554 self.changed.add(fname)
555 555 if copysource:
556 556 self.copied[fname] = copysource
557 557
558 558 def unlink(self, fname):
559 559 self._checkknown(fname)
560 560 self.removed.add(fname)
561 561
562 562 def exists(self, fname):
563 563 return fname in self.ctx
564 564
565 565 def close(self):
566 566 return self.changed | self.removed
567 567
568 568 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
569 569 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
570 570 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
571 571 eolmodes = ['strict', 'crlf', 'lf', 'auto']
572 572
573 573 class patchfile(object):
574 574 def __init__(self, ui, gp, backend, store, eolmode='strict'):
575 575 self.fname = gp.path
576 576 self.eolmode = eolmode
577 577 self.eol = None
578 578 self.backend = backend
579 579 self.ui = ui
580 580 self.lines = []
581 581 self.exists = False
582 582 self.missing = True
583 583 self.mode = gp.mode
584 584 self.copysource = gp.oldpath
585 585 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
586 586 self.remove = gp.op == 'DELETE'
587 587 try:
588 588 if self.copysource is None:
589 589 data, mode = backend.getfile(self.fname)
590 590 self.exists = True
591 591 else:
592 592 data, mode = store.getfile(self.copysource)[:2]
593 593 self.exists = backend.exists(self.fname)
594 594 self.missing = False
595 595 if data:
596 596 self.lines = mdiff.splitnewlines(data)
597 597 if self.mode is None:
598 598 self.mode = mode
599 599 if self.lines:
600 600 # Normalize line endings
601 601 if self.lines[0].endswith('\r\n'):
602 602 self.eol = '\r\n'
603 603 elif self.lines[0].endswith('\n'):
604 604 self.eol = '\n'
605 605 if eolmode != 'strict':
606 606 nlines = []
607 607 for l in self.lines:
608 608 if l.endswith('\r\n'):
609 609 l = l[:-2] + '\n'
610 610 nlines.append(l)
611 611 self.lines = nlines
612 612 except IOError:
613 613 if self.create:
614 614 self.missing = False
615 615 if self.mode is None:
616 616 self.mode = (False, False)
617 617 if self.missing:
618 618 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
619 619
620 620 self.hash = {}
621 621 self.dirty = 0
622 622 self.offset = 0
623 623 self.skew = 0
624 624 self.rej = []
625 625 self.fileprinted = False
626 626 self.printfile(False)
627 627 self.hunks = 0
628 628
629 629 def writelines(self, fname, lines, mode):
630 630 if self.eolmode == 'auto':
631 631 eol = self.eol
632 632 elif self.eolmode == 'crlf':
633 633 eol = '\r\n'
634 634 else:
635 635 eol = '\n'
636 636
637 637 if self.eolmode != 'strict' and eol and eol != '\n':
638 638 rawlines = []
639 639 for l in lines:
640 640 if l and l[-1] == '\n':
641 641 l = l[:-1] + eol
642 642 rawlines.append(l)
643 643 lines = rawlines
644 644
645 645 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
646 646
647 647 def printfile(self, warn):
648 648 if self.fileprinted:
649 649 return
650 650 if warn or self.ui.verbose:
651 651 self.fileprinted = True
652 652 s = _("patching file %s\n") % self.fname
653 653 if warn:
654 654 self.ui.warn(s)
655 655 else:
656 656 self.ui.note(s)
657 657
658 658
659 659 def findlines(self, l, linenum):
660 660 # looks through the hash and finds candidate lines. The
661 661 # result is a list of line numbers sorted based on distance
662 662 # from linenum
663 663
664 664 cand = self.hash.get(l, [])
665 665 if len(cand) > 1:
666 666 # resort our list of potentials forward then back.
667 667 cand.sort(key=lambda x: abs(x - linenum))
668 668 return cand
669 669
670 670 def write_rej(self):
671 671 # our rejects are a little different from patch(1). This always
672 672 # creates rejects in the same form as the original patch. A file
673 673 # header is inserted so that you can run the reject through patch again
674 674 # without having to type the filename.
675 675 if not self.rej:
676 676 return
677 677 base = os.path.basename(self.fname)
678 678 lines = ["--- %s\n+++ %s\n" % (base, base)]
679 679 for x in self.rej:
680 680 for l in x.hunk:
681 681 lines.append(l)
682 682 if l[-1] != '\n':
683 683 lines.append("\n\ No newline at end of file\n")
684 684 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
685 685
686 686 def apply(self, h):
687 687 if not h.complete():
688 688 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
689 689 (h.number, h.desc, len(h.a), h.lena, len(h.b),
690 690 h.lenb))
691 691
692 692 self.hunks += 1
693 693
694 694 if self.missing:
695 695 self.rej.append(h)
696 696 return -1
697 697
698 698 if self.exists and self.create:
699 699 if self.copysource:
700 700 self.ui.warn(_("cannot create %s: destination already "
701 701 "exists\n" % self.fname))
702 702 else:
703 703 self.ui.warn(_("file %s already exists\n") % self.fname)
704 704 self.rej.append(h)
705 705 return -1
706 706
707 707 if isinstance(h, binhunk):
708 708 if self.remove:
709 709 self.backend.unlink(self.fname)
710 710 else:
711 711 self.lines[:] = h.new()
712 712 self.offset += len(h.new())
713 713 self.dirty = True
714 714 return 0
715 715
716 716 horig = h
717 717 if (self.eolmode in ('crlf', 'lf')
718 718 or self.eolmode == 'auto' and self.eol):
719 719 # If new eols are going to be normalized, then normalize
720 720 # hunk data before patching. Otherwise, preserve input
721 721 # line-endings.
722 722 h = h.getnormalized()
723 723
724 724 # fast case first, no offsets, no fuzz
725 725 old = h.old()
726 726 # patch starts counting at 1 unless we are adding the file
727 727 if h.starta == 0:
728 728 start = 0
729 729 else:
730 730 start = h.starta + self.offset - 1
731 731 orig_start = start
732 732 # if there's skew we want to emit the "(offset %d lines)" even
733 733 # when the hunk cleanly applies at start + skew, so skip the
734 734 # fast case code
735 735 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
736 736 if self.remove:
737 737 self.backend.unlink(self.fname)
738 738 else:
739 739 self.lines[start : start + h.lena] = h.new()
740 740 self.offset += h.lenb - h.lena
741 741 self.dirty = True
742 742 return 0
743 743
744 744 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
745 745 self.hash = {}
746 746 for x, s in enumerate(self.lines):
747 747 self.hash.setdefault(s, []).append(x)
748 748 if h.hunk[-1][0] != ' ':
749 749 # if the hunk tried to put something at the bottom of the file
750 750 # override the start line and use eof here
751 751 search_start = len(self.lines)
752 752 else:
753 753 search_start = orig_start + self.skew
754 754
755 755 for fuzzlen in xrange(3):
756 756 for toponly in [True, False]:
757 757 old = h.old(fuzzlen, toponly)
758 758
759 759 cand = self.findlines(old[0][1:], search_start)
760 760 for l in cand:
761 761 if diffhelpers.testhunk(old, self.lines, l) == 0:
762 762 newlines = h.new(fuzzlen, toponly)
763 763 self.lines[l : l + len(old)] = newlines
764 764 self.offset += len(newlines) - len(old)
765 765 self.skew = l - orig_start
766 766 self.dirty = True
767 767 offset = l - orig_start - fuzzlen
768 768 if fuzzlen:
769 769 msg = _("Hunk #%d succeeded at %d "
770 770 "with fuzz %d "
771 771 "(offset %d lines).\n")
772 772 self.printfile(True)
773 773 self.ui.warn(msg %
774 774 (h.number, l + 1, fuzzlen, offset))
775 775 else:
776 776 msg = _("Hunk #%d succeeded at %d "
777 777 "(offset %d lines).\n")
778 778 self.ui.note(msg % (h.number, l + 1, offset))
779 779 return fuzzlen
780 780 self.printfile(True)
781 781 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
782 782 self.rej.append(horig)
783 783 return -1
784 784
785 785 def close(self):
786 786 if self.dirty:
787 787 self.writelines(self.fname, self.lines, self.mode)
788 788 self.write_rej()
789 789 return len(self.rej)
790 790
791 791 class hunk(object):
792 792 def __init__(self, desc, num, lr, context):
793 793 self.number = num
794 794 self.desc = desc
795 795 self.hunk = [desc]
796 796 self.a = []
797 797 self.b = []
798 798 self.starta = self.lena = None
799 799 self.startb = self.lenb = None
800 800 if lr is not None:
801 801 if context:
802 802 self.read_context_hunk(lr)
803 803 else:
804 804 self.read_unified_hunk(lr)
805 805
806 806 def getnormalized(self):
807 807 """Return a copy with line endings normalized to LF."""
808 808
809 809 def normalize(lines):
810 810 nlines = []
811 811 for line in lines:
812 812 if line.endswith('\r\n'):
813 813 line = line[:-2] + '\n'
814 814 nlines.append(line)
815 815 return nlines
816 816
817 817 # Dummy object, it is rebuilt manually
818 818 nh = hunk(self.desc, self.number, None, None)
819 819 nh.number = self.number
820 820 nh.desc = self.desc
821 821 nh.hunk = self.hunk
822 822 nh.a = normalize(self.a)
823 823 nh.b = normalize(self.b)
824 824 nh.starta = self.starta
825 825 nh.startb = self.startb
826 826 nh.lena = self.lena
827 827 nh.lenb = self.lenb
828 828 return nh
829 829
830 830 def read_unified_hunk(self, lr):
831 831 m = unidesc.match(self.desc)
832 832 if not m:
833 833 raise PatchError(_("bad hunk #%d") % self.number)
834 834 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
835 835 if self.lena is None:
836 836 self.lena = 1
837 837 else:
838 838 self.lena = int(self.lena)
839 839 if self.lenb is None:
840 840 self.lenb = 1
841 841 else:
842 842 self.lenb = int(self.lenb)
843 843 self.starta = int(self.starta)
844 844 self.startb = int(self.startb)
845 845 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
846 846 # if we hit eof before finishing out the hunk, the last line will
847 847 # be zero length. Lets try to fix it up.
848 848 while len(self.hunk[-1]) == 0:
849 849 del self.hunk[-1]
850 850 del self.a[-1]
851 851 del self.b[-1]
852 852 self.lena -= 1
853 853 self.lenb -= 1
854 854 self._fixnewline(lr)
855 855
856 856 def read_context_hunk(self, lr):
857 857 self.desc = lr.readline()
858 858 m = contextdesc.match(self.desc)
859 859 if not m:
860 860 raise PatchError(_("bad hunk #%d") % self.number)
861 861 foo, self.starta, foo2, aend, foo3 = m.groups()
862 862 self.starta = int(self.starta)
863 863 if aend is None:
864 864 aend = self.starta
865 865 self.lena = int(aend) - self.starta
866 866 if self.starta:
867 867 self.lena += 1
868 868 for x in xrange(self.lena):
869 869 l = lr.readline()
870 870 if l.startswith('---'):
871 871 # lines addition, old block is empty
872 872 lr.push(l)
873 873 break
874 874 s = l[2:]
875 875 if l.startswith('- ') or l.startswith('! '):
876 876 u = '-' + s
877 877 elif l.startswith(' '):
878 878 u = ' ' + s
879 879 else:
880 880 raise PatchError(_("bad hunk #%d old text line %d") %
881 881 (self.number, x))
882 882 self.a.append(u)
883 883 self.hunk.append(u)
884 884
885 885 l = lr.readline()
886 886 if l.startswith('\ '):
887 887 s = self.a[-1][:-1]
888 888 self.a[-1] = s
889 889 self.hunk[-1] = s
890 890 l = lr.readline()
891 891 m = contextdesc.match(l)
892 892 if not m:
893 893 raise PatchError(_("bad hunk #%d") % self.number)
894 894 foo, self.startb, foo2, bend, foo3 = m.groups()
895 895 self.startb = int(self.startb)
896 896 if bend is None:
897 897 bend = self.startb
898 898 self.lenb = int(bend) - self.startb
899 899 if self.startb:
900 900 self.lenb += 1
901 901 hunki = 1
902 902 for x in xrange(self.lenb):
903 903 l = lr.readline()
904 904 if l.startswith('\ '):
905 905 # XXX: the only way to hit this is with an invalid line range.
906 906 # The no-eol marker is not counted in the line range, but I
907 907 # guess there are diff(1) out there which behave differently.
908 908 s = self.b[-1][:-1]
909 909 self.b[-1] = s
910 910 self.hunk[hunki - 1] = s
911 911 continue
912 912 if not l:
913 913 # line deletions, new block is empty and we hit EOF
914 914 lr.push(l)
915 915 break
916 916 s = l[2:]
917 917 if l.startswith('+ ') or l.startswith('! '):
918 918 u = '+' + s
919 919 elif l.startswith(' '):
920 920 u = ' ' + s
921 921 elif len(self.b) == 0:
922 922 # line deletions, new block is empty
923 923 lr.push(l)
924 924 break
925 925 else:
926 926 raise PatchError(_("bad hunk #%d old text line %d") %
927 927 (self.number, x))
928 928 self.b.append(s)
929 929 while True:
930 930 if hunki >= len(self.hunk):
931 931 h = ""
932 932 else:
933 933 h = self.hunk[hunki]
934 934 hunki += 1
935 935 if h == u:
936 936 break
937 937 elif h.startswith('-'):
938 938 continue
939 939 else:
940 940 self.hunk.insert(hunki - 1, u)
941 941 break
942 942
943 943 if not self.a:
944 944 # this happens when lines were only added to the hunk
945 945 for x in self.hunk:
946 946 if x.startswith('-') or x.startswith(' '):
947 947 self.a.append(x)
948 948 if not self.b:
949 949 # this happens when lines were only deleted from the hunk
950 950 for x in self.hunk:
951 951 if x.startswith('+') or x.startswith(' '):
952 952 self.b.append(x[1:])
953 953 # @@ -start,len +start,len @@
954 954 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
955 955 self.startb, self.lenb)
956 956 self.hunk[0] = self.desc
957 957 self._fixnewline(lr)
958 958
959 959 def _fixnewline(self, lr):
960 960 l = lr.readline()
961 961 if l.startswith('\ '):
962 962 diffhelpers.fix_newline(self.hunk, self.a, self.b)
963 963 else:
964 964 lr.push(l)
965 965
966 966 def complete(self):
967 967 return len(self.a) == self.lena and len(self.b) == self.lenb
968 968
969 969 def fuzzit(self, l, fuzz, toponly):
970 970 # this removes context lines from the top and bottom of list 'l'. It
971 971 # checks the hunk to make sure only context lines are removed, and then
972 972 # returns a new shortened list of lines.
973 973 fuzz = min(fuzz, len(l)-1)
974 974 if fuzz:
975 975 top = 0
976 976 bot = 0
977 977 hlen = len(self.hunk)
978 978 for x in xrange(hlen - 1):
979 979 # the hunk starts with the @@ line, so use x+1
980 980 if self.hunk[x + 1][0] == ' ':
981 981 top += 1
982 982 else:
983 983 break
984 984 if not toponly:
985 985 for x in xrange(hlen - 1):
986 986 if self.hunk[hlen - bot - 1][0] == ' ':
987 987 bot += 1
988 988 else:
989 989 break
990 990
991 991 # top and bot now count context in the hunk
992 992 # adjust them if either one is short
993 993 context = max(top, bot, 3)
994 994 if bot < context:
995 995 bot = max(0, fuzz - (context - bot))
996 996 else:
997 997 bot = min(fuzz, bot)
998 998 if top < context:
999 999 top = max(0, fuzz - (context - top))
1000 1000 else:
1001 1001 top = min(fuzz, top)
1002 1002
1003 1003 return l[top:len(l)-bot]
1004 1004 return l
1005 1005
1006 1006 def old(self, fuzz=0, toponly=False):
1007 1007 return self.fuzzit(self.a, fuzz, toponly)
1008 1008
1009 1009 def new(self, fuzz=0, toponly=False):
1010 1010 return self.fuzzit(self.b, fuzz, toponly)
1011 1011
1012 1012 class binhunk(object):
1013 1013 'A binary patch file. Only understands literals so far.'
1014 1014 def __init__(self, lr):
1015 1015 self.text = None
1016 1016 self.hunk = ['GIT binary patch\n']
1017 1017 self._read(lr)
1018 1018
1019 1019 def complete(self):
1020 1020 return self.text is not None
1021 1021
1022 1022 def new(self):
1023 1023 return [self.text]
1024 1024
1025 1025 def _read(self, lr):
1026 1026 line = lr.readline()
1027 1027 self.hunk.append(line)
1028 1028 while line and not line.startswith('literal '):
1029 1029 line = lr.readline()
1030 1030 self.hunk.append(line)
1031 1031 if not line:
1032 1032 raise PatchError(_('could not extract binary patch'))
1033 1033 size = int(line[8:].rstrip())
1034 1034 dec = []
1035 1035 line = lr.readline()
1036 1036 self.hunk.append(line)
1037 1037 while len(line) > 1:
1038 1038 l = line[0]
1039 1039 if l <= 'Z' and l >= 'A':
1040 1040 l = ord(l) - ord('A') + 1
1041 1041 else:
1042 1042 l = ord(l) - ord('a') + 27
1043 1043 dec.append(base85.b85decode(line[1:-1])[:l])
1044 1044 line = lr.readline()
1045 1045 self.hunk.append(line)
1046 1046 text = zlib.decompress(''.join(dec))
1047 1047 if len(text) != size:
1048 1048 raise PatchError(_('binary patch is %d bytes, not %d') %
1049 1049 len(text), size)
1050 1050 self.text = text
1051 1051
1052 1052 def parsefilename(str):
1053 1053 # --- filename \t|space stuff
1054 1054 s = str[4:].rstrip('\r\n')
1055 1055 i = s.find('\t')
1056 1056 if i < 0:
1057 1057 i = s.find(' ')
1058 1058 if i < 0:
1059 1059 return s
1060 1060 return s[:i]
1061 1061
1062 1062 def pathstrip(path, strip):
1063 1063 pathlen = len(path)
1064 1064 i = 0
1065 1065 if strip == 0:
1066 1066 return '', path.rstrip()
1067 1067 count = strip
1068 1068 while count > 0:
1069 1069 i = path.find('/', i)
1070 1070 if i == -1:
1071 1071 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1072 1072 (count, strip, path))
1073 1073 i += 1
1074 1074 # consume '//' in the path
1075 1075 while i < pathlen - 1 and path[i] == '/':
1076 1076 i += 1
1077 1077 count -= 1
1078 1078 return path[:i].lstrip(), path[i:].rstrip()
1079 1079
1080 1080 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip):
1081 1081 nulla = afile_orig == "/dev/null"
1082 1082 nullb = bfile_orig == "/dev/null"
1083 1083 create = nulla and hunk.starta == 0 and hunk.lena == 0
1084 1084 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1085 1085 abase, afile = pathstrip(afile_orig, strip)
1086 1086 gooda = not nulla and backend.exists(afile)
1087 1087 bbase, bfile = pathstrip(bfile_orig, strip)
1088 1088 if afile == bfile:
1089 1089 goodb = gooda
1090 1090 else:
1091 1091 goodb = not nullb and backend.exists(bfile)
1092 1092 missing = not goodb and not gooda and not create
1093 1093
1094 1094 # some diff programs apparently produce patches where the afile is
1095 1095 # not /dev/null, but afile starts with bfile
1096 1096 abasedir = afile[:afile.rfind('/') + 1]
1097 1097 bbasedir = bfile[:bfile.rfind('/') + 1]
1098 1098 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1099 1099 and hunk.starta == 0 and hunk.lena == 0):
1100 1100 create = True
1101 1101 missing = False
1102 1102
1103 1103 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1104 1104 # diff is between a file and its backup. In this case, the original
1105 1105 # file should be patched (see original mpatch code).
1106 1106 isbackup = (abase == bbase and bfile.startswith(afile))
1107 1107 fname = None
1108 1108 if not missing:
1109 1109 if gooda and goodb:
1110 1110 fname = isbackup and afile or bfile
1111 1111 elif gooda:
1112 1112 fname = afile
1113 1113
1114 1114 if not fname:
1115 1115 if not nullb:
1116 1116 fname = isbackup and afile or bfile
1117 1117 elif not nulla:
1118 1118 fname = afile
1119 1119 else:
1120 1120 raise PatchError(_("undefined source and destination files"))
1121 1121
1122 1122 gp = patchmeta(fname)
1123 1123 if create:
1124 1124 gp.op = 'ADD'
1125 1125 elif remove:
1126 1126 gp.op = 'DELETE'
1127 1127 return gp
1128 1128
1129 1129 def scangitpatch(lr, firstline):
1130 1130 """
1131 1131 Git patches can emit:
1132 1132 - rename a to b
1133 1133 - change b
1134 1134 - copy a to c
1135 1135 - change c
1136 1136
1137 1137 We cannot apply this sequence as-is, the renamed 'a' could not be
1138 1138 found for it would have been renamed already. And we cannot copy
1139 1139 from 'b' instead because 'b' would have been changed already. So
1140 1140 we scan the git patch for copy and rename commands so we can
1141 1141 perform the copies ahead of time.
1142 1142 """
1143 1143 pos = 0
1144 1144 try:
1145 1145 pos = lr.fp.tell()
1146 1146 fp = lr.fp
1147 1147 except IOError:
1148 1148 fp = cStringIO.StringIO(lr.fp.read())
1149 1149 gitlr = linereader(fp)
1150 1150 gitlr.push(firstline)
1151 1151 gitpatches = readgitpatch(gitlr)
1152 1152 fp.seek(pos)
1153 1153 return gitpatches
1154 1154
1155 1155 def iterhunks(fp):
1156 1156 """Read a patch and yield the following events:
1157 1157 - ("file", afile, bfile, firsthunk): select a new target file.
1158 1158 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1159 1159 "file" event.
1160 1160 - ("git", gitchanges): current diff is in git format, gitchanges
1161 1161 maps filenames to gitpatch records. Unique event.
1162 1162 """
1163 1163 afile = ""
1164 1164 bfile = ""
1165 1165 state = None
1166 1166 hunknum = 0
1167 1167 emitfile = newfile = False
1168 1168 gitpatches = None
1169 1169
1170 1170 # our states
1171 1171 BFILE = 1
1172 1172 context = None
1173 1173 lr = linereader(fp)
1174 1174
1175 1175 while True:
1176 1176 x = lr.readline()
1177 1177 if not x:
1178 1178 break
1179 1179 if state == BFILE and (
1180 1180 (not context and x[0] == '@')
1181 1181 or (context is not False and x.startswith('***************'))
1182 1182 or x.startswith('GIT binary patch')):
1183 1183 gp = None
1184 1184 if (gitpatches and
1185 1185 (gitpatches[-1][0] == afile or gitpatches[-1][1] == bfile)):
1186 1186 gp = gitpatches.pop()[2]
1187 1187 if x.startswith('GIT binary patch'):
1188 1188 h = binhunk(lr)
1189 1189 else:
1190 1190 if context is None and x.startswith('***************'):
1191 1191 context = True
1192 1192 h = hunk(x, hunknum + 1, lr, context)
1193 1193 hunknum += 1
1194 1194 if emitfile:
1195 1195 emitfile = False
1196 1196 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1197 1197 yield 'hunk', h
1198 1198 elif x.startswith('diff --git'):
1199 1199 m = gitre.match(x)
1200 1200 if not m:
1201 1201 continue
1202 if gitpatches is None:
1202 if not gitpatches:
1203 1203 # scan whole input for git metadata
1204 1204 gitpatches = [('a/' + gp.path, 'b/' + gp.path, gp) for gp
1205 1205 in scangitpatch(lr, x)]
1206 1206 yield 'git', [g[2].copy() for g in gitpatches
1207 1207 if g[2].op in ('COPY', 'RENAME')]
1208 1208 gitpatches.reverse()
1209 1209 afile = 'a/' + m.group(1)
1210 1210 bfile = 'b/' + m.group(2)
1211 1211 while afile != gitpatches[-1][0] and bfile != gitpatches[-1][1]:
1212 1212 gp = gitpatches.pop()[2]
1213 1213 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1214 1214 gp = gitpatches[-1][2]
1215 1215 # copy/rename + modify should modify target, not source
1216 1216 if gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD') or gp.mode:
1217 1217 afile = bfile
1218 1218 newfile = True
1219 1219 elif x.startswith('---'):
1220 1220 # check for a unified diff
1221 1221 l2 = lr.readline()
1222 1222 if not l2.startswith('+++'):
1223 1223 lr.push(l2)
1224 1224 continue
1225 1225 newfile = True
1226 1226 context = False
1227 1227 afile = parsefilename(x)
1228 1228 bfile = parsefilename(l2)
1229 1229 elif x.startswith('***'):
1230 1230 # check for a context diff
1231 1231 l2 = lr.readline()
1232 1232 if not l2.startswith('---'):
1233 1233 lr.push(l2)
1234 1234 continue
1235 1235 l3 = lr.readline()
1236 1236 lr.push(l3)
1237 1237 if not l3.startswith("***************"):
1238 1238 lr.push(l2)
1239 1239 continue
1240 1240 newfile = True
1241 1241 context = True
1242 1242 afile = parsefilename(x)
1243 1243 bfile = parsefilename(l2)
1244 1244
1245 1245 if newfile:
1246 1246 newfile = False
1247 1247 emitfile = True
1248 1248 state = BFILE
1249 1249 hunknum = 0
1250 1250
1251 1251 while gitpatches:
1252 1252 gp = gitpatches.pop()[2]
1253 1253 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1254 1254
1255 1255 def applydiff(ui, fp, backend, store, strip=1, eolmode='strict'):
1256 1256 """Reads a patch from fp and tries to apply it.
1257 1257
1258 1258 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1259 1259 there was any fuzz.
1260 1260
1261 1261 If 'eolmode' is 'strict', the patch content and patched file are
1262 1262 read in binary mode. Otherwise, line endings are ignored when
1263 1263 patching then normalized according to 'eolmode'.
1264 1264 """
1265 1265 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1266 1266 eolmode=eolmode)
1267 1267
1268 1268 def _applydiff(ui, fp, patcher, backend, store, strip=1,
1269 1269 eolmode='strict'):
1270 1270
1271 1271 def pstrip(p):
1272 1272 return pathstrip(p, strip - 1)[1]
1273 1273
1274 1274 rejects = 0
1275 1275 err = 0
1276 1276 current_file = None
1277 1277
1278 1278 for state, values in iterhunks(fp):
1279 1279 if state == 'hunk':
1280 1280 if not current_file:
1281 1281 continue
1282 1282 ret = current_file.apply(values)
1283 1283 if ret > 0:
1284 1284 err = 1
1285 1285 elif state == 'file':
1286 1286 if current_file:
1287 1287 rejects += current_file.close()
1288 1288 current_file = None
1289 1289 afile, bfile, first_hunk, gp = values
1290 1290 if gp:
1291 1291 path = pstrip(gp.path)
1292 1292 gp.path = pstrip(gp.path)
1293 1293 if gp.oldpath:
1294 1294 gp.oldpath = pstrip(gp.oldpath)
1295 1295 else:
1296 1296 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1297 1297 if gp.op == 'RENAME':
1298 1298 backend.unlink(gp.oldpath)
1299 1299 if not first_hunk:
1300 1300 if gp.op == 'DELETE':
1301 1301 backend.unlink(gp.path)
1302 1302 continue
1303 1303 data, mode = None, None
1304 1304 if gp.op in ('RENAME', 'COPY'):
1305 1305 data, mode = store.getfile(gp.oldpath)[:2]
1306 1306 if gp.mode:
1307 1307 mode = gp.mode
1308 1308 if gp.op == 'ADD':
1309 1309 # Added files without content have no hunk and
1310 1310 # must be created
1311 1311 data = ''
1312 1312 if data or mode:
1313 1313 if (gp.op in ('ADD', 'RENAME', 'COPY')
1314 1314 and backend.exists(gp.path)):
1315 1315 raise PatchError(_("cannot create %s: destination "
1316 1316 "already exists") % gp.path)
1317 1317 backend.setfile(gp.path, data, mode, gp.oldpath)
1318 1318 continue
1319 1319 try:
1320 1320 current_file = patcher(ui, gp, backend, store,
1321 1321 eolmode=eolmode)
1322 1322 except PatchError, inst:
1323 1323 ui.warn(str(inst) + '\n')
1324 1324 current_file = None
1325 1325 rejects += 1
1326 1326 continue
1327 1327 elif state == 'git':
1328 1328 for gp in values:
1329 1329 path = pstrip(gp.oldpath)
1330 1330 data, mode = backend.getfile(path)
1331 1331 store.setfile(path, data, mode)
1332 1332 else:
1333 1333 raise util.Abort(_('unsupported parser state: %s') % state)
1334 1334
1335 1335 if current_file:
1336 1336 rejects += current_file.close()
1337 1337
1338 1338 if rejects:
1339 1339 return -1
1340 1340 return err
1341 1341
1342 1342 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1343 1343 similarity):
1344 1344 """use <patcher> to apply <patchname> to the working directory.
1345 1345 returns whether patch was applied with fuzz factor."""
1346 1346
1347 1347 fuzz = False
1348 1348 args = []
1349 1349 cwd = repo.root
1350 1350 if cwd:
1351 1351 args.append('-d %s' % util.shellquote(cwd))
1352 1352 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1353 1353 util.shellquote(patchname)))
1354 1354 try:
1355 1355 for line in fp:
1356 1356 line = line.rstrip()
1357 1357 ui.note(line + '\n')
1358 1358 if line.startswith('patching file '):
1359 1359 pf = util.parsepatchoutput(line)
1360 1360 printed_file = False
1361 1361 files.add(pf)
1362 1362 elif line.find('with fuzz') >= 0:
1363 1363 fuzz = True
1364 1364 if not printed_file:
1365 1365 ui.warn(pf + '\n')
1366 1366 printed_file = True
1367 1367 ui.warn(line + '\n')
1368 1368 elif line.find('saving rejects to file') >= 0:
1369 1369 ui.warn(line + '\n')
1370 1370 elif line.find('FAILED') >= 0:
1371 1371 if not printed_file:
1372 1372 ui.warn(pf + '\n')
1373 1373 printed_file = True
1374 1374 ui.warn(line + '\n')
1375 1375 finally:
1376 1376 if files:
1377 1377 cfiles = list(files)
1378 1378 cwd = repo.getcwd()
1379 1379 if cwd:
1380 1380 cfiles = [util.pathto(repo.root, cwd, f)
1381 1381 for f in cfiles]
1382 1382 scmutil.addremove(repo, cfiles, similarity=similarity)
1383 1383 code = fp.close()
1384 1384 if code:
1385 1385 raise PatchError(_("patch command failed: %s") %
1386 1386 util.explainexit(code)[0])
1387 1387 return fuzz
1388 1388
1389 1389 def patchbackend(ui, backend, patchobj, strip, files=None, eolmode='strict'):
1390 1390 if files is None:
1391 1391 files = set()
1392 1392 if eolmode is None:
1393 1393 eolmode = ui.config('patch', 'eol', 'strict')
1394 1394 if eolmode.lower() not in eolmodes:
1395 1395 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1396 1396 eolmode = eolmode.lower()
1397 1397
1398 1398 store = filestore()
1399 1399 try:
1400 1400 fp = open(patchobj, 'rb')
1401 1401 except TypeError:
1402 1402 fp = patchobj
1403 1403 try:
1404 1404 ret = applydiff(ui, fp, backend, store, strip=strip,
1405 1405 eolmode=eolmode)
1406 1406 finally:
1407 1407 if fp != patchobj:
1408 1408 fp.close()
1409 1409 files.update(backend.close())
1410 1410 store.close()
1411 1411 if ret < 0:
1412 1412 raise PatchError(_('patch failed to apply'))
1413 1413 return ret > 0
1414 1414
1415 1415 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1416 1416 similarity=0):
1417 1417 """use builtin patch to apply <patchobj> to the working directory.
1418 1418 returns whether patch was applied with fuzz factor."""
1419 1419 backend = workingbackend(ui, repo, similarity)
1420 1420 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1421 1421
1422 1422 def patchrepo(ui, repo, ctx, store, patchobj, strip, files=None,
1423 1423 eolmode='strict'):
1424 1424 backend = repobackend(ui, repo, ctx, store)
1425 1425 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1426 1426
1427 1427 def makememctx(repo, parents, text, user, date, branch, files, store,
1428 1428 editor=None):
1429 1429 def getfilectx(repo, memctx, path):
1430 1430 data, (islink, isexec), copied = store.getfile(path)
1431 1431 return context.memfilectx(path, data, islink=islink, isexec=isexec,
1432 1432 copied=copied)
1433 1433 extra = {}
1434 1434 if branch:
1435 1435 extra['branch'] = encoding.fromlocal(branch)
1436 1436 ctx = context.memctx(repo, parents, text, files, getfilectx, user,
1437 1437 date, extra)
1438 1438 if editor:
1439 1439 ctx._text = editor(repo, ctx, [])
1440 1440 return ctx
1441 1441
1442 1442 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1443 1443 similarity=0):
1444 1444 """Apply <patchname> to the working directory.
1445 1445
1446 1446 'eolmode' specifies how end of lines should be handled. It can be:
1447 1447 - 'strict': inputs are read in binary mode, EOLs are preserved
1448 1448 - 'crlf': EOLs are ignored when patching and reset to CRLF
1449 1449 - 'lf': EOLs are ignored when patching and reset to LF
1450 1450 - None: get it from user settings, default to 'strict'
1451 1451 'eolmode' is ignored when using an external patcher program.
1452 1452
1453 1453 Returns whether patch was applied with fuzz factor.
1454 1454 """
1455 1455 patcher = ui.config('ui', 'patch')
1456 1456 if files is None:
1457 1457 files = set()
1458 1458 try:
1459 1459 if patcher:
1460 1460 return _externalpatch(ui, repo, patcher, patchname, strip,
1461 1461 files, similarity)
1462 1462 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1463 1463 similarity)
1464 1464 except PatchError, err:
1465 1465 raise util.Abort(str(err))
1466 1466
1467 1467 def changedfiles(ui, repo, patchpath, strip=1):
1468 1468 backend = fsbackend(ui, repo.root)
1469 1469 fp = open(patchpath, 'rb')
1470 1470 try:
1471 1471 changed = set()
1472 1472 for state, values in iterhunks(fp):
1473 1473 if state == 'file':
1474 1474 afile, bfile, first_hunk, gp = values
1475 1475 if gp:
1476 1476 gp.path = pathstrip(gp.path, strip - 1)[1]
1477 1477 if gp.oldpath:
1478 1478 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1479 1479 else:
1480 1480 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1481 1481 changed.add(gp.path)
1482 1482 if gp.op == 'RENAME':
1483 1483 changed.add(gp.oldpath)
1484 1484 elif state not in ('hunk', 'git'):
1485 1485 raise util.Abort(_('unsupported parser state: %s') % state)
1486 1486 return changed
1487 1487 finally:
1488 1488 fp.close()
1489 1489
1490 1490 def b85diff(to, tn):
1491 1491 '''print base85-encoded binary diff'''
1492 1492 def gitindex(text):
1493 1493 if not text:
1494 1494 return hex(nullid)
1495 1495 l = len(text)
1496 1496 s = util.sha1('blob %d\0' % l)
1497 1497 s.update(text)
1498 1498 return s.hexdigest()
1499 1499
1500 1500 def fmtline(line):
1501 1501 l = len(line)
1502 1502 if l <= 26:
1503 1503 l = chr(ord('A') + l - 1)
1504 1504 else:
1505 1505 l = chr(l - 26 + ord('a') - 1)
1506 1506 return '%c%s\n' % (l, base85.b85encode(line, True))
1507 1507
1508 1508 def chunk(text, csize=52):
1509 1509 l = len(text)
1510 1510 i = 0
1511 1511 while i < l:
1512 1512 yield text[i:i + csize]
1513 1513 i += csize
1514 1514
1515 1515 tohash = gitindex(to)
1516 1516 tnhash = gitindex(tn)
1517 1517 if tohash == tnhash:
1518 1518 return ""
1519 1519
1520 1520 # TODO: deltas
1521 1521 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1522 1522 (tohash, tnhash, len(tn))]
1523 1523 for l in chunk(zlib.compress(tn)):
1524 1524 ret.append(fmtline(l))
1525 1525 ret.append('\n')
1526 1526 return ''.join(ret)
1527 1527
1528 1528 class GitDiffRequired(Exception):
1529 1529 pass
1530 1530
1531 1531 def diffopts(ui, opts=None, untrusted=False):
1532 1532 def get(key, name=None, getter=ui.configbool):
1533 1533 return ((opts and opts.get(key)) or
1534 1534 getter('diff', name or key, None, untrusted=untrusted))
1535 1535 return mdiff.diffopts(
1536 1536 text=opts and opts.get('text'),
1537 1537 git=get('git'),
1538 1538 nodates=get('nodates'),
1539 1539 showfunc=get('show_function', 'showfunc'),
1540 1540 ignorews=get('ignore_all_space', 'ignorews'),
1541 1541 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1542 1542 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1543 1543 context=get('unified', getter=ui.config))
1544 1544
1545 1545 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1546 1546 losedatafn=None, prefix=''):
1547 1547 '''yields diff of changes to files between two nodes, or node and
1548 1548 working directory.
1549 1549
1550 1550 if node1 is None, use first dirstate parent instead.
1551 1551 if node2 is None, compare node1 with working directory.
1552 1552
1553 1553 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1554 1554 every time some change cannot be represented with the current
1555 1555 patch format. Return False to upgrade to git patch format, True to
1556 1556 accept the loss or raise an exception to abort the diff. It is
1557 1557 called with the name of current file being diffed as 'fn'. If set
1558 1558 to None, patches will always be upgraded to git format when
1559 1559 necessary.
1560 1560
1561 1561 prefix is a filename prefix that is prepended to all filenames on
1562 1562 display (used for subrepos).
1563 1563 '''
1564 1564
1565 1565 if opts is None:
1566 1566 opts = mdiff.defaultopts
1567 1567
1568 1568 if not node1 and not node2:
1569 1569 node1 = repo.dirstate.p1()
1570 1570
1571 1571 def lrugetfilectx():
1572 1572 cache = {}
1573 1573 order = []
1574 1574 def getfilectx(f, ctx):
1575 1575 fctx = ctx.filectx(f, filelog=cache.get(f))
1576 1576 if f not in cache:
1577 1577 if len(cache) > 20:
1578 1578 del cache[order.pop(0)]
1579 1579 cache[f] = fctx.filelog()
1580 1580 else:
1581 1581 order.remove(f)
1582 1582 order.append(f)
1583 1583 return fctx
1584 1584 return getfilectx
1585 1585 getfilectx = lrugetfilectx()
1586 1586
1587 1587 ctx1 = repo[node1]
1588 1588 ctx2 = repo[node2]
1589 1589
1590 1590 if not changes:
1591 1591 changes = repo.status(ctx1, ctx2, match=match)
1592 1592 modified, added, removed = changes[:3]
1593 1593
1594 1594 if not modified and not added and not removed:
1595 1595 return []
1596 1596
1597 1597 revs = None
1598 1598 if not repo.ui.quiet:
1599 1599 hexfunc = repo.ui.debugflag and hex or short
1600 1600 revs = [hexfunc(node) for node in [node1, node2] if node]
1601 1601
1602 1602 copy = {}
1603 1603 if opts.git or opts.upgrade:
1604 1604 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1605 1605
1606 1606 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1607 1607 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1608 1608 if opts.upgrade and not opts.git:
1609 1609 try:
1610 1610 def losedata(fn):
1611 1611 if not losedatafn or not losedatafn(fn=fn):
1612 1612 raise GitDiffRequired()
1613 1613 # Buffer the whole output until we are sure it can be generated
1614 1614 return list(difffn(opts.copy(git=False), losedata))
1615 1615 except GitDiffRequired:
1616 1616 return difffn(opts.copy(git=True), None)
1617 1617 else:
1618 1618 return difffn(opts, None)
1619 1619
1620 1620 def difflabel(func, *args, **kw):
1621 1621 '''yields 2-tuples of (output, label) based on the output of func()'''
1622 1622 prefixes = [('diff', 'diff.diffline'),
1623 1623 ('copy', 'diff.extended'),
1624 1624 ('rename', 'diff.extended'),
1625 1625 ('old', 'diff.extended'),
1626 1626 ('new', 'diff.extended'),
1627 1627 ('deleted', 'diff.extended'),
1628 1628 ('---', 'diff.file_a'),
1629 1629 ('+++', 'diff.file_b'),
1630 1630 ('@@', 'diff.hunk'),
1631 1631 ('-', 'diff.deleted'),
1632 1632 ('+', 'diff.inserted')]
1633 1633
1634 1634 for chunk in func(*args, **kw):
1635 1635 lines = chunk.split('\n')
1636 1636 for i, line in enumerate(lines):
1637 1637 if i != 0:
1638 1638 yield ('\n', '')
1639 1639 stripline = line
1640 1640 if line and line[0] in '+-':
1641 1641 # highlight trailing whitespace, but only in changed lines
1642 1642 stripline = line.rstrip()
1643 1643 for prefix, label in prefixes:
1644 1644 if stripline.startswith(prefix):
1645 1645 yield (stripline, label)
1646 1646 break
1647 1647 else:
1648 1648 yield (line, '')
1649 1649 if line != stripline:
1650 1650 yield (line[len(stripline):], 'diff.trailingwhitespace')
1651 1651
1652 1652 def diffui(*args, **kw):
1653 1653 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1654 1654 return difflabel(diff, *args, **kw)
1655 1655
1656 1656
1657 1657 def _addmodehdr(header, omode, nmode):
1658 1658 if omode != nmode:
1659 1659 header.append('old mode %s\n' % omode)
1660 1660 header.append('new mode %s\n' % nmode)
1661 1661
1662 1662 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1663 1663 copy, getfilectx, opts, losedatafn, prefix):
1664 1664
1665 1665 def join(f):
1666 1666 return os.path.join(prefix, f)
1667 1667
1668 1668 date1 = util.datestr(ctx1.date())
1669 1669 man1 = ctx1.manifest()
1670 1670
1671 1671 gone = set()
1672 1672 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1673 1673
1674 1674 copyto = dict([(v, k) for k, v in copy.items()])
1675 1675
1676 1676 if opts.git:
1677 1677 revs = None
1678 1678
1679 1679 for f in sorted(modified + added + removed):
1680 1680 to = None
1681 1681 tn = None
1682 1682 dodiff = True
1683 1683 header = []
1684 1684 if f in man1:
1685 1685 to = getfilectx(f, ctx1).data()
1686 1686 if f not in removed:
1687 1687 tn = getfilectx(f, ctx2).data()
1688 1688 a, b = f, f
1689 1689 if opts.git or losedatafn:
1690 1690 if f in added:
1691 1691 mode = gitmode[ctx2.flags(f)]
1692 1692 if f in copy or f in copyto:
1693 1693 if opts.git:
1694 1694 if f in copy:
1695 1695 a = copy[f]
1696 1696 else:
1697 1697 a = copyto[f]
1698 1698 omode = gitmode[man1.flags(a)]
1699 1699 _addmodehdr(header, omode, mode)
1700 1700 if a in removed and a not in gone:
1701 1701 op = 'rename'
1702 1702 gone.add(a)
1703 1703 else:
1704 1704 op = 'copy'
1705 1705 header.append('%s from %s\n' % (op, join(a)))
1706 1706 header.append('%s to %s\n' % (op, join(f)))
1707 1707 to = getfilectx(a, ctx1).data()
1708 1708 else:
1709 1709 losedatafn(f)
1710 1710 else:
1711 1711 if opts.git:
1712 1712 header.append('new file mode %s\n' % mode)
1713 1713 elif ctx2.flags(f):
1714 1714 losedatafn(f)
1715 1715 # In theory, if tn was copied or renamed we should check
1716 1716 # if the source is binary too but the copy record already
1717 1717 # forces git mode.
1718 1718 if util.binary(tn):
1719 1719 if opts.git:
1720 1720 dodiff = 'binary'
1721 1721 else:
1722 1722 losedatafn(f)
1723 1723 if not opts.git and not tn:
1724 1724 # regular diffs cannot represent new empty file
1725 1725 losedatafn(f)
1726 1726 elif f in removed:
1727 1727 if opts.git:
1728 1728 # have we already reported a copy above?
1729 1729 if ((f in copy and copy[f] in added
1730 1730 and copyto[copy[f]] == f) or
1731 1731 (f in copyto and copyto[f] in added
1732 1732 and copy[copyto[f]] == f)):
1733 1733 dodiff = False
1734 1734 else:
1735 1735 header.append('deleted file mode %s\n' %
1736 1736 gitmode[man1.flags(f)])
1737 1737 elif not to or util.binary(to):
1738 1738 # regular diffs cannot represent empty file deletion
1739 1739 losedatafn(f)
1740 1740 else:
1741 1741 oflag = man1.flags(f)
1742 1742 nflag = ctx2.flags(f)
1743 1743 binary = util.binary(to) or util.binary(tn)
1744 1744 if opts.git:
1745 1745 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1746 1746 if binary:
1747 1747 dodiff = 'binary'
1748 1748 elif binary or nflag != oflag:
1749 1749 losedatafn(f)
1750 1750 if opts.git:
1751 1751 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1752 1752
1753 1753 if dodiff:
1754 1754 if dodiff == 'binary':
1755 1755 text = b85diff(to, tn)
1756 1756 else:
1757 1757 text = mdiff.unidiff(to, date1,
1758 1758 # ctx2 date may be dynamic
1759 1759 tn, util.datestr(ctx2.date()),
1760 1760 join(a), join(b), revs, opts=opts)
1761 1761 if header and (text or len(header) > 1):
1762 1762 yield ''.join(header)
1763 1763 if text:
1764 1764 yield text
1765 1765
1766 1766 def diffstatsum(stats):
1767 1767 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
1768 1768 for f, a, r, b in stats:
1769 1769 maxfile = max(maxfile, encoding.colwidth(f))
1770 1770 maxtotal = max(maxtotal, a + r)
1771 1771 addtotal += a
1772 1772 removetotal += r
1773 1773 binary = binary or b
1774 1774
1775 1775 return maxfile, maxtotal, addtotal, removetotal, binary
1776 1776
1777 1777 def diffstatdata(lines):
1778 1778 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1779 1779
1780 1780 results = []
1781 1781 filename, adds, removes = None, 0, 0
1782 1782
1783 1783 def addresult():
1784 1784 if filename:
1785 1785 isbinary = adds == 0 and removes == 0
1786 1786 results.append((filename, adds, removes, isbinary))
1787 1787
1788 1788 for line in lines:
1789 1789 if line.startswith('diff'):
1790 1790 addresult()
1791 1791 # set numbers to 0 anyway when starting new file
1792 1792 adds, removes = 0, 0
1793 1793 if line.startswith('diff --git'):
1794 1794 filename = gitre.search(line).group(1)
1795 1795 elif line.startswith('diff -r'):
1796 1796 # format: "diff -r ... -r ... filename"
1797 1797 filename = diffre.search(line).group(1)
1798 1798 elif line.startswith('+') and not line.startswith('+++'):
1799 1799 adds += 1
1800 1800 elif line.startswith('-') and not line.startswith('---'):
1801 1801 removes += 1
1802 1802 addresult()
1803 1803 return results
1804 1804
1805 1805 def diffstat(lines, width=80, git=False):
1806 1806 output = []
1807 1807 stats = diffstatdata(lines)
1808 1808 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
1809 1809
1810 1810 countwidth = len(str(maxtotal))
1811 1811 if hasbinary and countwidth < 3:
1812 1812 countwidth = 3
1813 1813 graphwidth = width - countwidth - maxname - 6
1814 1814 if graphwidth < 10:
1815 1815 graphwidth = 10
1816 1816
1817 1817 def scale(i):
1818 1818 if maxtotal <= graphwidth:
1819 1819 return i
1820 1820 # If diffstat runs out of room it doesn't print anything,
1821 1821 # which isn't very useful, so always print at least one + or -
1822 1822 # if there were at least some changes.
1823 1823 return max(i * graphwidth // maxtotal, int(bool(i)))
1824 1824
1825 1825 for filename, adds, removes, isbinary in stats:
1826 1826 if git and isbinary:
1827 1827 count = 'Bin'
1828 1828 else:
1829 1829 count = adds + removes
1830 1830 pluses = '+' * scale(adds)
1831 1831 minuses = '-' * scale(removes)
1832 1832 output.append(' %s%s | %*s %s%s\n' %
1833 1833 (filename, ' ' * (maxname - encoding.colwidth(filename)),
1834 1834 countwidth, count, pluses, minuses))
1835 1835
1836 1836 if stats:
1837 1837 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1838 1838 % (len(stats), totaladds, totalremoves))
1839 1839
1840 1840 return ''.join(output)
1841 1841
1842 1842 def diffstatui(*args, **kw):
1843 1843 '''like diffstat(), but yields 2-tuples of (output, label) for
1844 1844 ui.write()
1845 1845 '''
1846 1846
1847 1847 for line in diffstat(*args, **kw).splitlines():
1848 1848 if line and line[-1] in '+-':
1849 1849 name, graph = line.rsplit(' ', 1)
1850 1850 yield (name + ' ', '')
1851 1851 m = re.search(r'\++', graph)
1852 1852 if m:
1853 1853 yield (m.group(0), 'diffstat.inserted')
1854 1854 m = re.search(r'-+', graph)
1855 1855 if m:
1856 1856 yield (m.group(0), 'diffstat.deleted')
1857 1857 else:
1858 1858 yield (line, '')
1859 1859 yield ('\n', '')
@@ -1,733 +1,733
1 1 # ui.py - user interface bits for mercurial
2 2 #
3 3 # Copyright 2005-2007 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 from i18n import _
9 9 import errno, getpass, os, socket, sys, tempfile, traceback
10 10 import config, scmutil, util, error
11 11
12 12 class ui(object):
13 13 def __init__(self, src=None):
14 14 self._buffers = []
15 15 self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
16 16 self._reportuntrusted = True
17 17 self._ocfg = config.config() # overlay
18 18 self._tcfg = config.config() # trusted
19 19 self._ucfg = config.config() # untrusted
20 20 self._trustusers = set()
21 21 self._trustgroups = set()
22 22
23 23 if src:
24 24 self.fout = src.fout
25 25 self.ferr = src.ferr
26 26 self.fin = src.fin
27 27
28 28 self._tcfg = src._tcfg.copy()
29 29 self._ucfg = src._ucfg.copy()
30 30 self._ocfg = src._ocfg.copy()
31 31 self._trustusers = src._trustusers.copy()
32 32 self._trustgroups = src._trustgroups.copy()
33 33 self.environ = src.environ
34 34 self.fixconfig()
35 35 else:
36 36 self.fout = sys.stdout
37 37 self.ferr = sys.stderr
38 38 self.fin = sys.stdin
39 39
40 40 # shared read-only environment
41 41 self.environ = os.environ
42 42 # we always trust global config files
43 43 for f in scmutil.rcpath():
44 44 self.readconfig(f, trust=True)
45 45
46 46 def copy(self):
47 47 return self.__class__(self)
48 48
49 49 def _trusted(self, fp, f):
50 50 st = util.fstat(fp)
51 51 if util.isowner(st):
52 52 return True
53 53
54 54 tusers, tgroups = self._trustusers, self._trustgroups
55 55 if '*' in tusers or '*' in tgroups:
56 56 return True
57 57
58 58 user = util.username(st.st_uid)
59 59 group = util.groupname(st.st_gid)
60 60 if user in tusers or group in tgroups or user == util.username():
61 61 return True
62 62
63 63 if self._reportuntrusted:
64 64 self.warn(_('Not trusting file %s from untrusted '
65 65 'user %s, group %s\n') % (f, user, group))
66 66 return False
67 67
68 68 def readconfig(self, filename, root=None, trust=False,
69 69 sections=None, remap=None):
70 70 try:
71 71 fp = open(filename)
72 72 except IOError:
73 73 if not sections: # ignore unless we were looking for something
74 74 return
75 75 raise
76 76
77 77 cfg = config.config()
78 78 trusted = sections or trust or self._trusted(fp, filename)
79 79
80 80 try:
81 81 cfg.read(filename, fp, sections=sections, remap=remap)
82 82 except error.ConfigError, inst:
83 83 if trusted:
84 84 raise
85 85 self.warn(_("Ignored: %s\n") % str(inst))
86 86
87 87 if self.plain():
88 88 for k in ('debug', 'fallbackencoding', 'quiet', 'slash',
89 89 'logtemplate', 'style',
90 90 'traceback', 'verbose'):
91 91 if k in cfg['ui']:
92 92 del cfg['ui'][k]
93 93 for k, v in cfg.items('defaults'):
94 94 del cfg['defaults'][k]
95 95 # Don't remove aliases from the configuration if in the exceptionlist
96 96 if self.plain('alias'):
97 97 for k, v in cfg.items('alias'):
98 98 del cfg['alias'][k]
99 99
100 100 if trusted:
101 101 self._tcfg.update(cfg)
102 102 self._tcfg.update(self._ocfg)
103 103 self._ucfg.update(cfg)
104 104 self._ucfg.update(self._ocfg)
105 105
106 106 if root is None:
107 107 root = os.path.expanduser('~')
108 108 self.fixconfig(root=root)
109 109
110 110 def fixconfig(self, root=None, section=None):
111 111 if section in (None, 'paths'):
112 112 # expand vars and ~
113 113 # translate paths relative to root (or home) into absolute paths
114 114 root = root or os.getcwd()
115 115 for c in self._tcfg, self._ucfg, self._ocfg:
116 116 for n, p in c.items('paths'):
117 117 if not p:
118 118 continue
119 119 if '%%' in p:
120 120 self.warn(_("(deprecated '%%' in path %s=%s from %s)\n")
121 121 % (n, p, self.configsource('paths', n)))
122 122 p = p.replace('%%', '%')
123 123 p = util.expandpath(p)
124 124 if not util.hasscheme(p) and not os.path.isabs(p):
125 125 p = os.path.normpath(os.path.join(root, p))
126 126 c.set("paths", n, p)
127 127
128 128 if section in (None, 'ui'):
129 129 # update ui options
130 130 self.debugflag = self.configbool('ui', 'debug')
131 131 self.verbose = self.debugflag or self.configbool('ui', 'verbose')
132 132 self.quiet = not self.debugflag and self.configbool('ui', 'quiet')
133 133 if self.verbose and self.quiet:
134 134 self.quiet = self.verbose = False
135 135 self._reportuntrusted = self.debugflag or self.configbool("ui",
136 136 "report_untrusted", True)
137 137 self.tracebackflag = self.configbool('ui', 'traceback', False)
138 138
139 139 if section in (None, 'trusted'):
140 140 # update trust information
141 141 self._trustusers.update(self.configlist('trusted', 'users'))
142 142 self._trustgroups.update(self.configlist('trusted', 'groups'))
143 143
144 144 def setconfig(self, section, name, value, overlay=True):
145 145 if overlay:
146 146 self._ocfg.set(section, name, value)
147 147 self._tcfg.set(section, name, value)
148 148 self._ucfg.set(section, name, value)
149 149 self.fixconfig(section=section)
150 150
151 151 def _data(self, untrusted):
152 152 return untrusted and self._ucfg or self._tcfg
153 153
154 154 def configsource(self, section, name, untrusted=False):
155 155 return self._data(untrusted).source(section, name) or 'none'
156 156
157 157 def config(self, section, name, default=None, untrusted=False):
158 158 if isinstance(name, list):
159 159 alternates = name
160 160 else:
161 161 alternates = [name]
162 162
163 163 for n in alternates:
164 164 value = self._data(untrusted).get(section, name, None)
165 165 if value is not None:
166 166 name = n
167 167 break
168 168 else:
169 169 value = default
170 170
171 171 if self.debugflag and not untrusted and self._reportuntrusted:
172 172 uvalue = self._ucfg.get(section, name)
173 173 if uvalue is not None and uvalue != value:
174 174 self.debug("ignoring untrusted configuration option "
175 175 "%s.%s = %s\n" % (section, name, uvalue))
176 176 return value
177 177
178 178 def configpath(self, section, name, default=None, untrusted=False):
179 179 'get a path config item, expanded relative to repo root or config file'
180 180 v = self.config(section, name, default, untrusted)
181 181 if v is None:
182 182 return None
183 183 if not os.path.isabs(v) or "://" not in v:
184 184 src = self.configsource(section, name, untrusted)
185 185 if ':' in src:
186 186 base = os.path.dirname(src.rsplit(':')[0])
187 187 v = os.path.join(base, os.path.expanduser(v))
188 188 return v
189 189
190 190 def configbool(self, section, name, default=False, untrusted=False):
191 191 """parse a configuration element as a boolean
192 192
193 193 >>> u = ui(); s = 'foo'
194 194 >>> u.setconfig(s, 'true', 'yes')
195 195 >>> u.configbool(s, 'true')
196 196 True
197 197 >>> u.setconfig(s, 'false', 'no')
198 198 >>> u.configbool(s, 'false')
199 199 False
200 200 >>> u.configbool(s, 'unknown')
201 201 False
202 202 >>> u.configbool(s, 'unknown', True)
203 203 True
204 204 >>> u.setconfig(s, 'invalid', 'somevalue')
205 205 >>> u.configbool(s, 'invalid')
206 206 Traceback (most recent call last):
207 207 ...
208 208 ConfigError: foo.invalid is not a boolean ('somevalue')
209 209 """
210 210
211 211 v = self.config(section, name, None, untrusted)
212 212 if v is None:
213 213 return default
214 214 if isinstance(v, bool):
215 215 return v
216 216 b = util.parsebool(v)
217 217 if b is None:
218 218 raise error.ConfigError(_("%s.%s is not a boolean ('%s')")
219 219 % (section, name, v))
220 220 return b
221 221
222 222 def configint(self, section, name, default=None, untrusted=False):
223 223 """parse a configuration element as an integer
224 224
225 225 >>> u = ui(); s = 'foo'
226 226 >>> u.setconfig(s, 'int1', '42')
227 227 >>> u.configint(s, 'int1')
228 228 42
229 229 >>> u.setconfig(s, 'int2', '-42')
230 230 >>> u.configint(s, 'int2')
231 231 -42
232 232 >>> u.configint(s, 'unknown', 7)
233 233 7
234 234 >>> u.setconfig(s, 'invalid', 'somevalue')
235 235 >>> u.configint(s, 'invalid')
236 236 Traceback (most recent call last):
237 237 ...
238 238 ConfigError: foo.invalid is not an integer ('somevalue')
239 239 """
240 240
241 241 v = self.config(section, name, None, untrusted)
242 242 if v is None:
243 243 return default
244 244 try:
245 245 return int(v)
246 246 except ValueError:
247 247 raise error.ConfigError(_("%s.%s is not an integer ('%s')")
248 248 % (section, name, v))
249 249
250 250 def configlist(self, section, name, default=None, untrusted=False):
251 251 """parse a configuration element as a list of comma/space separated
252 252 strings
253 253
254 254 >>> u = ui(); s = 'foo'
255 255 >>> u.setconfig(s, 'list1', 'this,is "a small" ,test')
256 256 >>> u.configlist(s, 'list1')
257 257 ['this', 'is', 'a small', 'test']
258 258 """
259 259
260 260 def _parse_plain(parts, s, offset):
261 261 whitespace = False
262 262 while offset < len(s) and (s[offset].isspace() or s[offset] == ','):
263 263 whitespace = True
264 264 offset += 1
265 265 if offset >= len(s):
266 266 return None, parts, offset
267 267 if whitespace:
268 268 parts.append('')
269 269 if s[offset] == '"' and not parts[-1]:
270 270 return _parse_quote, parts, offset + 1
271 271 elif s[offset] == '"' and parts[-1][-1] == '\\':
272 272 parts[-1] = parts[-1][:-1] + s[offset]
273 273 return _parse_plain, parts, offset + 1
274 274 parts[-1] += s[offset]
275 275 return _parse_plain, parts, offset + 1
276 276
277 277 def _parse_quote(parts, s, offset):
278 278 if offset < len(s) and s[offset] == '"': # ""
279 279 parts.append('')
280 280 offset += 1
281 281 while offset < len(s) and (s[offset].isspace() or
282 282 s[offset] == ','):
283 283 offset += 1
284 284 return _parse_plain, parts, offset
285 285
286 286 while offset < len(s) and s[offset] != '"':
287 287 if (s[offset] == '\\' and offset + 1 < len(s)
288 288 and s[offset + 1] == '"'):
289 289 offset += 1
290 290 parts[-1] += '"'
291 291 else:
292 292 parts[-1] += s[offset]
293 293 offset += 1
294 294
295 295 if offset >= len(s):
296 296 real_parts = _configlist(parts[-1])
297 297 if not real_parts:
298 298 parts[-1] = '"'
299 299 else:
300 300 real_parts[0] = '"' + real_parts[0]
301 301 parts = parts[:-1]
302 302 parts.extend(real_parts)
303 303 return None, parts, offset
304 304
305 305 offset += 1
306 306 while offset < len(s) and s[offset] in [' ', ',']:
307 307 offset += 1
308 308
309 309 if offset < len(s):
310 310 if offset + 1 == len(s) and s[offset] == '"':
311 311 parts[-1] += '"'
312 312 offset += 1
313 313 else:
314 314 parts.append('')
315 315 else:
316 316 return None, parts, offset
317 317
318 318 return _parse_plain, parts, offset
319 319
320 320 def _configlist(s):
321 321 s = s.rstrip(' ,')
322 322 if not s:
323 323 return []
324 324 parser, parts, offset = _parse_plain, [''], 0
325 325 while parser:
326 326 parser, parts, offset = parser(parts, s, offset)
327 327 return parts
328 328
329 329 result = self.config(section, name, untrusted=untrusted)
330 330 if result is None:
331 331 result = default or []
332 332 if isinstance(result, basestring):
333 333 result = _configlist(result.lstrip(' ,\n'))
334 334 if result is None:
335 335 result = default or []
336 336 return result
337 337
338 338 def has_section(self, section, untrusted=False):
339 339 '''tell whether section exists in config.'''
340 340 return section in self._data(untrusted)
341 341
342 342 def configitems(self, section, untrusted=False):
343 343 items = self._data(untrusted).items(section)
344 344 if self.debugflag and not untrusted and self._reportuntrusted:
345 345 for k, v in self._ucfg.items(section):
346 346 if self._tcfg.get(section, k) != v:
347 347 self.debug("ignoring untrusted configuration option "
348 348 "%s.%s = %s\n" % (section, k, v))
349 349 return items
350 350
351 351 def walkconfig(self, untrusted=False):
352 352 cfg = self._data(untrusted)
353 353 for section in cfg.sections():
354 354 for name, value in self.configitems(section, untrusted):
355 355 yield section, name, value
356 356
357 357 def plain(self, feature=None):
358 358 '''is plain mode active?
359 359
360 360 Plain mode means that all configuration variables which affect
361 361 the behavior and output of Mercurial should be
362 362 ignored. Additionally, the output should be stable,
363 363 reproducible and suitable for use in scripts or applications.
364 364
365 365 The only way to trigger plain mode is by setting either the
366 366 `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
367 367
368 368 The return value can either be
369 369 - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
370 370 - True otherwise
371 371 '''
372 372 if 'HGPLAIN' not in os.environ and 'HGPLAINEXCEPT' not in os.environ:
373 373 return False
374 374 exceptions = os.environ.get('HGPLAINEXCEPT', '').strip().split(',')
375 375 if feature and exceptions:
376 376 return feature not in exceptions
377 377 return True
378 378
379 379 def username(self):
380 380 """Return default username to be used in commits.
381 381
382 382 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL
383 383 and stop searching if one of these is set.
384 384 If not found and ui.askusername is True, ask the user, else use
385 385 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname".
386 386 """
387 387 user = os.environ.get("HGUSER")
388 388 if user is None:
389 389 user = self.config("ui", "username")
390 390 if user is not None:
391 391 user = os.path.expandvars(user)
392 392 if user is None:
393 393 user = os.environ.get("EMAIL")
394 394 if user is None and self.configbool("ui", "askusername"):
395 395 user = self.prompt(_("enter a commit username:"), default=None)
396 396 if user is None and not self.interactive():
397 397 try:
398 398 user = '%s@%s' % (util.getuser(), socket.getfqdn())
399 399 self.warn(_("No username found, using '%s' instead\n") % user)
400 400 except KeyError:
401 401 pass
402 402 if not user:
403 403 raise util.Abort(_('no username supplied (see "hg help config")'))
404 404 if "\n" in user:
405 405 raise util.Abort(_("username %s contains a newline\n") % repr(user))
406 406 return user
407 407
408 408 def shortuser(self, user):
409 409 """Return a short representation of a user name or email address."""
410 410 if not self.verbose:
411 411 user = util.shortuser(user)
412 412 return user
413 413
414 414 def expandpath(self, loc, default=None):
415 415 """Return repository location relative to cwd or from [paths]"""
416 416 if util.hasscheme(loc) or os.path.isdir(os.path.join(loc, '.hg')):
417 417 return loc
418 418
419 419 path = self.config('paths', loc)
420 420 if not path and default is not None:
421 421 path = self.config('paths', default)
422 422 return path or loc
423 423
424 424 def pushbuffer(self):
425 425 self._buffers.append([])
426 426
427 427 def popbuffer(self, labeled=False):
428 428 '''pop the last buffer and return the buffered output
429 429
430 430 If labeled is True, any labels associated with buffered
431 431 output will be handled. By default, this has no effect
432 432 on the output returned, but extensions and GUI tools may
433 433 handle this argument and returned styled output. If output
434 434 is being buffered so it can be captured and parsed or
435 435 processed, labeled should not be set to True.
436 436 '''
437 437 return "".join(self._buffers.pop())
438 438
439 439 def write(self, *args, **opts):
440 440 '''write args to output
441 441
442 442 By default, this method simply writes to the buffer or stdout,
443 443 but extensions or GUI tools may override this method,
444 444 write_err(), popbuffer(), and label() to style output from
445 445 various parts of hg.
446 446
447 447 An optional keyword argument, "label", can be passed in.
448 448 This should be a string containing label names separated by
449 449 space. Label names take the form of "topic.type". For example,
450 450 ui.debug() issues a label of "ui.debug".
451 451
452 452 When labeling output for a specific command, a label of
453 453 "cmdname.type" is recommended. For example, status issues
454 454 a label of "status.modified" for modified files.
455 455 '''
456 456 if self._buffers:
457 457 self._buffers[-1].extend([str(a) for a in args])
458 458 else:
459 459 for a in args:
460 460 self.fout.write(str(a))
461 461
462 462 def write_err(self, *args, **opts):
463 463 try:
464 464 if not getattr(self.fout, 'closed', False):
465 465 self.fout.flush()
466 466 for a in args:
467 467 self.ferr.write(str(a))
468 468 # stderr may be buffered under win32 when redirected to files,
469 469 # including stdout.
470 470 if not getattr(self.ferr, 'closed', False):
471 471 self.ferr.flush()
472 472 except IOError, inst:
473 473 if inst.errno not in (errno.EPIPE, errno.EIO):
474 474 raise
475 475
476 476 def flush(self):
477 477 try: self.fout.flush()
478 478 except: pass
479 479 try: self.ferr.flush()
480 480 except: pass
481 481
482 482 def interactive(self):
483 483 '''is interactive input allowed?
484 484
485 485 An interactive session is a session where input can be reasonably read
486 486 from `sys.stdin'. If this function returns false, any attempt to read
487 487 from stdin should fail with an error, unless a sensible default has been
488 488 specified.
489 489
490 490 Interactiveness is triggered by the value of the `ui.interactive'
491 491 configuration variable or - if it is unset - when `sys.stdin' points
492 492 to a terminal device.
493 493
494 494 This function refers to input only; for output, see `ui.formatted()'.
495 495 '''
496 496 i = self.configbool("ui", "interactive", None)
497 497 if i is None:
498 498 # some environments replace stdin without implementing isatty
499 499 # usually those are non-interactive
500 500 return util.isatty(self.fin)
501 501
502 502 return i
503 503
504 504 def termwidth(self):
505 505 '''how wide is the terminal in columns?
506 506 '''
507 507 if 'COLUMNS' in os.environ:
508 508 try:
509 509 return int(os.environ['COLUMNS'])
510 510 except ValueError:
511 511 pass
512 512 return util.termwidth()
513 513
514 514 def formatted(self):
515 515 '''should formatted output be used?
516 516
517 517 It is often desirable to format the output to suite the output medium.
518 518 Examples of this are truncating long lines or colorizing messages.
519 519 However, this is not often not desirable when piping output into other
520 520 utilities, e.g. `grep'.
521 521
522 522 Formatted output is triggered by the value of the `ui.formatted'
523 523 configuration variable or - if it is unset - when `sys.stdout' points
524 524 to a terminal device. Please note that `ui.formatted' should be
525 525 considered an implementation detail; it is not intended for use outside
526 526 Mercurial or its extensions.
527 527
528 528 This function refers to output only; for input, see `ui.interactive()'.
529 529 This function always returns false when in plain mode, see `ui.plain()'.
530 530 '''
531 531 if self.plain():
532 532 return False
533 533
534 534 i = self.configbool("ui", "formatted", None)
535 535 if i is None:
536 536 # some environments replace stdout without implementing isatty
537 537 # usually those are non-interactive
538 538 return util.isatty(self.fout)
539 539
540 540 return i
541 541
542 542 def _readline(self, prompt=''):
543 543 if util.isatty(self.fin):
544 544 try:
545 545 # magically add command line editing support, where
546 546 # available
547 547 import readline
548 548 # force demandimport to really load the module
549 549 readline.read_history_file
550 550 # windows sometimes raises something other than ImportError
551 551 except Exception:
552 552 pass
553 553
554 554 # call write() so output goes through subclassed implementation
555 555 # e.g. color extension on Windows
556 556 self.write(prompt)
557 557
558 558 # instead of trying to emulate raw_input, swap (self.fin,
559 559 # self.fout) with (sys.stdin, sys.stdout)
560 560 oldin = sys.stdin
561 561 oldout = sys.stdout
562 562 sys.stdin = self.fin
563 563 sys.stdout = self.fout
564 564 line = raw_input(' ')
565 565 sys.stdin = oldin
566 566 sys.stdout = oldout
567 567
568 568 # When stdin is in binary mode on Windows, it can cause
569 569 # raw_input() to emit an extra trailing carriage return
570 570 if os.linesep == '\r\n' and line and line[-1] == '\r':
571 571 line = line[:-1]
572 572 return line
573 573
574 574 def prompt(self, msg, default="y"):
575 575 """Prompt user with msg, read response.
576 576 If ui is not interactive, the default is returned.
577 577 """
578 578 if not self.interactive():
579 579 self.write(msg, ' ', default, "\n")
580 580 return default
581 581 try:
582 582 r = self._readline(self.label(msg, 'ui.prompt'))
583 583 if not r:
584 584 return default
585 585 return r
586 586 except EOFError:
587 587 raise util.Abort(_('response expected'))
588 588
589 589 def promptchoice(self, msg, choices, default=0):
590 590 """Prompt user with msg, read response, and ensure it matches
591 591 one of the provided choices. The index of the choice is returned.
592 592 choices is a sequence of acceptable responses with the format:
593 593 ('&None', 'E&xec', 'Sym&link') Responses are case insensitive.
594 594 If ui is not interactive, the default is returned.
595 595 """
596 596 resps = [s[s.index('&')+1].lower() for s in choices]
597 597 while True:
598 598 r = self.prompt(msg, resps[default])
599 599 if r.lower() in resps:
600 600 return resps.index(r.lower())
601 601 self.write(_("unrecognized response\n"))
602 602
603 603 def getpass(self, prompt=None, default=None):
604 604 if not self.interactive():
605 605 return default
606 606 try:
607 607 return getpass.getpass(prompt or _('password: '))
608 608 except EOFError:
609 609 raise util.Abort(_('response expected'))
610 610 def status(self, *msg, **opts):
611 611 '''write status message to output (if ui.quiet is False)
612 612
613 613 This adds an output label of "ui.status".
614 614 '''
615 615 if not self.quiet:
616 616 opts['label'] = opts.get('label', '') + ' ui.status'
617 617 self.write(*msg, **opts)
618 618 def warn(self, *msg, **opts):
619 619 '''write warning message to output (stderr)
620 620
621 621 This adds an output label of "ui.warning".
622 622 '''
623 623 opts['label'] = opts.get('label', '') + ' ui.warning'
624 624 self.write_err(*msg, **opts)
625 625 def note(self, *msg, **opts):
626 626 '''write note to output (if ui.verbose is True)
627 627
628 628 This adds an output label of "ui.note".
629 629 '''
630 630 if self.verbose:
631 631 opts['label'] = opts.get('label', '') + ' ui.note'
632 632 self.write(*msg, **opts)
633 633 def debug(self, *msg, **opts):
634 634 '''write debug message to output (if ui.debugflag is True)
635 635
636 636 This adds an output label of "ui.debug".
637 637 '''
638 638 if self.debugflag:
639 639 opts['label'] = opts.get('label', '') + ' ui.debug'
640 640 self.write(*msg, **opts)
641 641 def edit(self, text, user):
642 642 (fd, name) = tempfile.mkstemp(prefix="hg-editor-", suffix=".txt",
643 643 text=True)
644 644 try:
645 645 f = os.fdopen(fd, "w")
646 646 f.write(text)
647 647 f.close()
648 648
649 649 editor = self.geteditor()
650 650
651 651 util.system("%s \"%s\"" % (editor, name),
652 652 environ={'HGUSER': user},
653 653 onerr=util.Abort, errprefix=_("edit failed"),
654 654 out=self.fout)
655 655
656 656 f = open(name)
657 657 t = f.read()
658 658 f.close()
659 659 finally:
660 660 os.unlink(name)
661 661
662 662 return t
663 663
664 664 def traceback(self, exc=None):
665 665 '''print exception traceback if traceback printing enabled.
666 666 only to call in exception handler. returns true if traceback
667 667 printed.'''
668 668 if self.tracebackflag:
669 669 if exc:
670 traceback.print_exception(exc[0], exc[1], exc[2])
670 traceback.print_exception(exc[0], exc[1], exc[2], file=self.ferr)
671 671 else:
672 traceback.print_exc()
672 traceback.print_exc(file=self.ferr)
673 673 return self.tracebackflag
674 674
675 675 def geteditor(self):
676 676 '''return editor to use'''
677 677 return (os.environ.get("HGEDITOR") or
678 678 self.config("ui", "editor") or
679 679 os.environ.get("VISUAL") or
680 680 os.environ.get("EDITOR", "vi"))
681 681
682 682 def progress(self, topic, pos, item="", unit="", total=None):
683 683 '''show a progress message
684 684
685 685 With stock hg, this is simply a debug message that is hidden
686 686 by default, but with extensions or GUI tools it may be
687 687 visible. 'topic' is the current operation, 'item' is a
688 688 non-numeric marker of the current position (ie the currently
689 689 in-process file), 'pos' is the current numeric position (ie
690 690 revision, bytes, etc.), unit is a corresponding unit label,
691 691 and total is the highest expected pos.
692 692
693 693 Multiple nested topics may be active at a time.
694 694
695 695 All topics should be marked closed by setting pos to None at
696 696 termination.
697 697 '''
698 698
699 699 if pos is None or not self.debugflag:
700 700 return
701 701
702 702 if unit:
703 703 unit = ' ' + unit
704 704 if item:
705 705 item = ' ' + item
706 706
707 707 if total:
708 708 pct = 100.0 * pos / total
709 709 self.debug('%s:%s %s/%s%s (%4.2f%%)\n'
710 710 % (topic, item, pos, total, unit, pct))
711 711 else:
712 712 self.debug('%s:%s %s%s\n' % (topic, item, pos, unit))
713 713
714 714 def log(self, service, message):
715 715 '''hook for logging facility extensions
716 716
717 717 service should be a readily-identifiable subsystem, which will
718 718 allow filtering.
719 719 message should be a newline-terminated string to log.
720 720 '''
721 721 pass
722 722
723 723 def label(self, msg, label):
724 724 '''style msg based on supplied label
725 725
726 726 Like ui.write(), this just returns msg unchanged, but extensions
727 727 and GUI tools can override it to allow styling output without
728 728 writing it.
729 729
730 730 ui.write(s, 'label') is equivalent to
731 731 ui.write(ui.label(s, 'label')).
732 732 '''
733 733 return msg
@@ -1,326 +1,326
1 1
2 2 Function to test discovery between two repos in both directions, using both the local shortcut
3 3 (which is currently not activated by default) and the full remotable protocol:
4 4
5 5 $ testdesc() { # revs_a, revs_b, dagdesc
6 6 > if [ -d foo ]; then rm -rf foo; fi
7 7 > hg init foo
8 8 > cd foo
9 9 > hg debugbuilddag "$3"
10 10 > hg clone . a $1 --quiet
11 11 > hg clone . b $2 --quiet
12 12 > echo
13 13 > echo "% -- a -> b tree"
14 14 > hg -R a debugdiscovery b --verbose --old
15 15 > echo
16 16 > echo "% -- a -> b set"
17 17 > hg -R a debugdiscovery b --verbose --debug
18 18 > echo
19 19 > echo "% -- b -> a tree"
20 20 > hg -R b debugdiscovery a --verbose --old
21 21 > echo
22 22 > echo "% -- b -> a set"
23 23 > hg -R b debugdiscovery a --verbose --debug
24 24 > cd ..
25 25 > }
26 26
27 27
28 28 Small superset:
29 29
30 30 $ testdesc '-ra1 -ra2' '-rb1 -rb2 -rb3' '
31 31 > +2:f +1:a1:b1
32 32 > <f +4 :a2
33 33 > +5 :b2
34 34 > <f +3 :b3'
35 35
36 36 % -- a -> b tree
37 37 comparing with b
38 38 searching for changes
39 39 unpruned common: b5714e113bc0 66f7d451a68b 01241442b3c2
40 40 common heads: b5714e113bc0 01241442b3c2
41 41 local is subset
42 42
43 43 % -- a -> b set
44 44 comparing with b
45 45 query 1; heads
46 46 searching for changes
47 47 all local heads known remotely
48 48 common heads: b5714e113bc0 01241442b3c2
49 49 local is subset
50 50
51 51 % -- b -> a tree
52 52 comparing with a
53 53 searching for changes
54 54 unpruned common: b5714e113bc0 01241442b3c2
55 55 common heads: b5714e113bc0 01241442b3c2
56 56 remote is subset
57 57
58 58 % -- b -> a set
59 59 comparing with a
60 60 query 1; heads
61 61 searching for changes
62 62 all remote heads known locally
63 63 common heads: b5714e113bc0 01241442b3c2
64 64 remote is subset
65 65
66 66
67 67 Many new:
68 68
69 69 $ testdesc '-ra1 -ra2' '-rb' '
70 70 > +2:f +3:a1 +3:b
71 71 > <f +30 :a2'
72 72
73 73 % -- a -> b tree
74 74 comparing with b
75 75 searching for changes
76 76 unpruned common: bebd167eb94d
77 77 common heads: bebd167eb94d
78 78
79 79 % -- a -> b set
80 80 comparing with b
81 81 query 1; heads
82 82 searching for changes
83 83 taking initial sample
84 84 searching: 2 queries
85 85 query 2; still undecided: 29, sample size is: 29
86 86 2 total queries
87 87 common heads: bebd167eb94d
88 88
89 89 % -- b -> a tree
90 90 comparing with a
91 91 searching for changes
92 92 unpruned common: bebd167eb94d 66f7d451a68b
93 93 common heads: bebd167eb94d
94 94
95 95 % -- b -> a set
96 96 comparing with a
97 97 query 1; heads
98 98 searching for changes
99 99 taking initial sample
100 100 searching: 2 queries
101 101 query 2; still undecided: 2, sample size is: 2
102 102 2 total queries
103 103 common heads: bebd167eb94d
104 104
105 105
106 106 Both sides many new with stub:
107 107
108 108 $ testdesc '-ra1 -ra2' '-rb' '
109 109 > +2:f +2:a1 +30 :b
110 110 > <f +30 :a2'
111 111
112 112 % -- a -> b tree
113 113 comparing with b
114 114 searching for changes
115 115 unpruned common: 2dc09a01254d
116 116 common heads: 2dc09a01254d
117 117
118 118 % -- a -> b set
119 119 comparing with b
120 120 query 1; heads
121 121 searching for changes
122 122 taking initial sample
123 123 searching: 2 queries
124 124 query 2; still undecided: 29, sample size is: 29
125 125 2 total queries
126 126 common heads: 2dc09a01254d
127 127
128 128 % -- b -> a tree
129 129 comparing with a
130 130 searching for changes
131 131 unpruned common: 66f7d451a68b 2dc09a01254d
132 132 common heads: 2dc09a01254d
133 133
134 134 % -- b -> a set
135 135 comparing with a
136 136 query 1; heads
137 137 searching for changes
138 138 taking initial sample
139 139 searching: 2 queries
140 140 query 2; still undecided: 29, sample size is: 29
141 141 2 total queries
142 142 common heads: 2dc09a01254d
143 143
144 144
145 145 Both many new:
146 146
147 147 $ testdesc '-ra' '-rb' '
148 148 > +2:f +30 :b
149 149 > <f +30 :a'
150 150
151 151 % -- a -> b tree
152 152 comparing with b
153 153 searching for changes
154 154 unpruned common: 66f7d451a68b
155 155 common heads: 66f7d451a68b
156 156
157 157 % -- a -> b set
158 158 comparing with b
159 159 query 1; heads
160 160 searching for changes
161 161 taking quick initial sample
162 162 searching: 2 queries
163 163 query 2; still undecided: 31, sample size is: 31
164 164 2 total queries
165 165 common heads: 66f7d451a68b
166 166
167 167 % -- b -> a tree
168 168 comparing with a
169 169 searching for changes
170 170 unpruned common: 66f7d451a68b
171 171 common heads: 66f7d451a68b
172 172
173 173 % -- b -> a set
174 174 comparing with a
175 175 query 1; heads
176 176 searching for changes
177 177 taking quick initial sample
178 178 searching: 2 queries
179 179 query 2; still undecided: 31, sample size is: 31
180 180 2 total queries
181 181 common heads: 66f7d451a68b
182 182
183 183
184 184 Both many new skewed:
185 185
186 186 $ testdesc '-ra' '-rb' '
187 187 > +2:f +30 :b
188 188 > <f +50 :a'
189 189
190 190 % -- a -> b tree
191 191 comparing with b
192 192 searching for changes
193 193 unpruned common: 66f7d451a68b
194 194 common heads: 66f7d451a68b
195 195
196 196 % -- a -> b set
197 197 comparing with b
198 198 query 1; heads
199 199 searching for changes
200 200 taking quick initial sample
201 201 searching: 2 queries
202 202 query 2; still undecided: 51, sample size is: 51
203 203 2 total queries
204 204 common heads: 66f7d451a68b
205 205
206 206 % -- b -> a tree
207 207 comparing with a
208 208 searching for changes
209 209 unpruned common: 66f7d451a68b
210 210 common heads: 66f7d451a68b
211 211
212 212 % -- b -> a set
213 213 comparing with a
214 214 query 1; heads
215 215 searching for changes
216 216 taking quick initial sample
217 217 searching: 2 queries
218 218 query 2; still undecided: 31, sample size is: 31
219 219 2 total queries
220 220 common heads: 66f7d451a68b
221 221
222 222
223 223 Both many new on top of long history:
224 224
225 225 $ testdesc '-ra' '-rb' '
226 226 > +1000:f +30 :b
227 227 > <f +50 :a'
228 228
229 229 % -- a -> b tree
230 230 comparing with b
231 231 searching for changes
232 232 unpruned common: 7ead0cba2838
233 233 common heads: 7ead0cba2838
234 234
235 235 % -- a -> b set
236 236 comparing with b
237 237 query 1; heads
238 238 searching for changes
239 239 taking quick initial sample
240 240 searching: 2 queries
241 241 query 2; still undecided: 1049, sample size is: 11
242 242 sampling from both directions
243 243 searching: 3 queries
244 244 query 3; still undecided: 31, sample size is: 31
245 245 3 total queries
246 246 common heads: 7ead0cba2838
247 247
248 248 % -- b -> a tree
249 249 comparing with a
250 250 searching for changes
251 251 unpruned common: 7ead0cba2838
252 252 common heads: 7ead0cba2838
253 253
254 254 % -- b -> a set
255 255 comparing with a
256 256 query 1; heads
257 257 searching for changes
258 258 taking quick initial sample
259 259 searching: 2 queries
260 260 query 2; still undecided: 1029, sample size is: 11
261 261 sampling from both directions
262 262 searching: 3 queries
263 263 query 3; still undecided: 15, sample size is: 15
264 264 3 total queries
265 265 common heads: 7ead0cba2838
266 266
267 267
268 268 One with >200 heads, which used to use up all of the sample:
269 269
270 270 $ hg init manyheads
271 271 $ cd manyheads
272 272 $ echo "+300:r @a" >dagdesc
273 273 $ echo "*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3 *r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3" >>dagdesc # 20 heads
274 274 $ echo "*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3 *r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3" >>dagdesc # 20 heads
275 275 $ echo "*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3 *r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3" >>dagdesc # 20 heads
276 276 $ echo "*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3 *r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3" >>dagdesc # 20 heads
277 277 $ echo "*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3 *r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3" >>dagdesc # 20 heads
278 278 $ echo "*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3 *r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3" >>dagdesc # 20 heads
279 279 $ echo "*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3 *r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3" >>dagdesc # 20 heads
280 280 $ echo "*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3 *r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3" >>dagdesc # 20 heads
281 281 $ echo "*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3 *r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3" >>dagdesc # 20 heads
282 282 $ echo "*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3 *r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3" >>dagdesc # 20 heads
283 283 $ echo "*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3 *r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3" >>dagdesc # 20 heads
284 284 $ echo "*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3 *r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3" >>dagdesc # 20 heads
285 285 $ echo "*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3 *r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3*r+3" >>dagdesc # 20 heads
286 286 $ echo "@b *r+3" >>dagdesc # one more head
287 287 $ hg debugbuilddag <dagdesc
288 288 reading DAG from stdin
289 289
290 290 $ hg heads -t --template . | wc -c
291 261
291 *261 (re)
292 292
293 293 $ hg clone -b a . a
294 294 adding changesets
295 295 adding manifests
296 296 adding file changes
297 297 added 1340 changesets with 0 changes to 0 files (+259 heads)
298 298 updating to branch a
299 299 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
300 300 $ hg clone -b b . b
301 301 adding changesets
302 302 adding manifests
303 303 adding file changes
304 304 added 304 changesets with 0 changes to 0 files
305 305 updating to branch b
306 306 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
307 307
308 308 $ hg -R a debugdiscovery b --debug --verbose
309 309 comparing with b
310 310 query 1; heads
311 311 searching for changes
312 312 taking quick initial sample
313 313 searching: 2 queries
314 314 query 2; still undecided: 1080, sample size is: 260
315 315 sampling from both directions
316 316 searching: 3 queries
317 317 query 3; still undecided: 820, sample size is: 260
318 318 sampling from both directions
319 319 searching: 4 queries
320 320 query 4; still undecided: 560, sample size is: 260
321 321 sampling from both directions
322 322 searching: 5 queries
323 323 query 5; still undecided: 300, sample size is: 200
324 324 5 total queries
325 325 common heads: 3ee37d65064a
326 326
General Comments 0
You need to be logged in to leave comments. Login now